// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only

part of '../../document_migration.dart';

/// Pure V0-to-V1 migration service.
///
/// Version 0 is the pre-Block-15 document shape: no `schemaVersion`, no owner
/// metadata, enum values stored as Dart `.name`, and no exact backlog-entry
/// timestamp. Missing `schemaVersion` is treated as V0; unsupported future
/// versions fail closed.
class V1DocumentMigrationService {
  /// Creates a `V1DocumentMigrationService` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  const V1DocumentMigrationService({
    required this.ownerId,
    required this.migratedAt,
  });

  /// Stores the `ownerId` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final String ownerId;

  /// Stores the `migratedAt` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final DateTime migratedAt;

  /// Performs the `migrateTask` behavior for this scheduler component.
  /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
  DocumentMigrationResult migrateTask(Map<String, Object?> document) {
    return _migrate(
      entity: DocumentMigrationEntity.task,
      document: document,
      migrateV0: _taskV0ToV1,
      validateCurrent: TaskDocumentMapping.fromDocument,
    );
  }

  /// Performs the `migrateProject` behavior for this scheduler component.
  /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
  DocumentMigrationResult migrateProject(Map<String, Object?> document) {
    return _migrate(
      entity: DocumentMigrationEntity.project,
      document: document,
      migrateV0: _projectV0ToV1,
      validateCurrent: ProjectDocumentMapping.fromDocument,
    );
  }

  /// Performs the `migrateLockedBlock` behavior for this scheduler component.
  /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
  DocumentMigrationResult migrateLockedBlock(Map<String, Object?> document) {
    return _migrate(
      entity: DocumentMigrationEntity.lockedBlock,
      document: document,
      migrateV0: _lockedBlockV0ToV1,
      validateCurrent: LockedBlockDocumentMapping.fromDocument,
    );
  }

  /// Performs the `migrateLockedBlockOverride` behavior for this scheduler component.
  /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
  DocumentMigrationResult migrateLockedBlockOverride(
    Map<String, Object?> document,
  ) {
    return _migrate(
      entity: DocumentMigrationEntity.lockedBlockOverride,
      document: document,
      migrateV0: _lockedBlockOverrideV0ToV1,
      validateCurrent: LockedBlockOverrideDocumentMapping.fromDocument,
    );
  }

  /// Runs the `_migrate` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  DocumentMigrationResult _migrate({
    required DocumentMigrationEntity entity,
    required Map<String, Object?> document,
    required _V0Migrator migrateV0,
    required void Function(Map<String, Object?> document) validateCurrent,
  }) {
    final documentId = _documentId(document);
    logger.debug(() => 'Document migration started. '
        'entity=${entity.name} documentId=$documentId '
        'ownerId=$ownerId migratedAt=${migratedAt.toIso8601String()}');
    final schemaRead = _readSchemaVersion(document);
    if (schemaRead.failure != null) {
      logger.warn(() => 'Document migration failed schema version read. '
          'entity=${entity.name} documentId=$documentId '
          'detailCode=${schemaRead.failure}');
      return _failed(
        entity: entity,
        documentId: documentId,
        fromSchemaVersion: null,
        failureCode: DocumentMigrationFailureCode.invalidSchemaVersion,
        fieldName: DocumentFields.schemaVersion,
        detailCode: schemaRead.failure,
      );
    }

    final fromVersion = schemaRead.version;
    logger.finer(() => 'Document migration schema version resolved. '
        'entity=${entity.name} documentId=$documentId fromSchemaVersion=$fromVersion');
    if (fromVersion == v1SchemaVersion) {
      try {
        final copy = _deepCopyDocument(document);
        validateCurrent(copy);
        logger.debug(() =>
            'Document migration skipped; document already current. '
            'entity=${entity.name} documentId=$documentId fromSchemaVersion=$fromVersion');
        return DocumentMigrationResult(
          document: copy,
          report: DocumentMigrationReport(
            entity: entity,
            status: DocumentMigrationStatus.alreadyCurrent,
            documentId: documentId,
            fromSchemaVersion: fromVersion,
          ),
        );
      } on DocumentMappingException catch (error) {
        logger.warn(
            () => 'Document migration failed current document validation. '
                'entity=${entity.name} documentId=$documentId '
                'fromSchemaVersion=$fromVersion mappingCode=${error.code.name} '
                'fieldName=${error.fieldName} detailCode=${error.detailCode}');
        return _failedFromMapping(
          entity: entity,
          documentId: documentId,
          fromSchemaVersion: fromVersion,
          failureCode: DocumentMigrationFailureCode.currentDocumentFailure,
          error: error,
        );
      }
    }

    if (fromVersion > v1SchemaVersion || fromVersion < 0) {
      logger.warn(() =>
          'Document migration rejected unsupported schema version. '
          'entity=${entity.name} documentId=$documentId fromSchemaVersion=$fromVersion');
      return _failed(
        entity: entity,
        documentId: documentId,
        fromSchemaVersion: fromVersion,
        failureCode: DocumentMigrationFailureCode.unsupportedSchemaVersion,
        fieldName: DocumentFields.schemaVersion,
        detailCode: fromVersion.toString(),
      );
    }

    if (fromVersion != 0) {
      logger.warn(() =>
          'Document migration rejected unsupported schema version. '
          'entity=${entity.name} documentId=$documentId fromSchemaVersion=$fromVersion');
      return _failed(
        entity: entity,
        documentId: documentId,
        fromSchemaVersion: fromVersion,
        failureCode: DocumentMigrationFailureCode.unsupportedSchemaVersion,
        fieldName: DocumentFields.schemaVersion,
        detailCode: fromVersion.toString(),
      );
    }

    final notes = <DocumentMigrationNote>[];
    late final Map<String, Object?> migrated;
    try {
      migrated = migrateV0(document, notes);
    } on _LegacyDocumentException catch (error) {
      logger.warn(() => 'Document migration failed legacy document validation. '
          'entity=${entity.name} documentId=$documentId '
          'fromSchemaVersion=$fromVersion fieldName=${error.fieldName} '
          'detailCode=${error.detailCode}');
      return _failed(
        entity: entity,
        documentId: documentId,
        fromSchemaVersion: fromVersion,
        failureCode: DocumentMigrationFailureCode.legacyDocumentFailure,
        fieldName: error.fieldName,
        detailCode: error.detailCode,
      );
    }

    try {
      validateCurrent(migrated);
    } on DocumentMappingException catch (error) {
      logger
          .warn(() => 'Document migration failed migrated document validation. '
              'entity=${entity.name} documentId=$documentId '
              'fromSchemaVersion=$fromVersion mappingCode=${error.code.name} '
              'fieldName=${error.fieldName} detailCode=${error.detailCode}');
      return _failedFromMapping(
        entity: entity,
        documentId: documentId,
        fromSchemaVersion: fromVersion,
        failureCode: DocumentMigrationFailureCode.currentDocumentFailure,
        error: error,
      );
    }

    logger.debug(() => 'Document migration finished. '
        'entity=${entity.name} documentId=$documentId fromSchemaVersion=$fromVersion '
        'toSchemaVersion=$v1SchemaVersion noteCount=${notes.length}');
    return DocumentMigrationResult(
      document: migrated,
      report: DocumentMigrationReport(
        entity: entity,
        status: DocumentMigrationStatus.migrated,
        documentId: documentId,
        fromSchemaVersion: fromVersion,
        notes: List<DocumentMigrationNote>.unmodifiable(notes),
      ),
    );
  }

  /// Runs the `_taskV0ToV1` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  Map<String, Object?> _taskV0ToV1(
    Map<String, Object?> document,
    List<DocumentMigrationNote> notes,
  ) {
    final createdAt = _requiredInstant(document, TaskDocumentFields.createdAt);
    final status = _requiredLegacyCode(
      document,
      TaskDocumentFields.status,
      _taskStatusCodes,
    );
    final isBacklog = status == 'backlog';
    if (isBacklog) {
      notes.add(
        const DocumentMigrationNote(
          code: DocumentMigrationNoteCode.approximatedBacklogEnteredAt,
          fieldName: TaskDocumentFields.backlogEnteredAt,
          detailCode: DocumentMigrationProvenance.approximatedFromCreatedAt,
        ),
      );
    }

    return {
      ..._commonV1Fields(
        id: _requiredString(document, TaskDocumentFields.id),
        createdAt: createdAt,
        updatedAt: _requiredInstant(document, TaskDocumentFields.updatedAt),
      ),
      TaskDocumentFields.title:
          _requiredString(document, TaskDocumentFields.title),
      TaskDocumentFields.projectId:
          _requiredString(document, TaskDocumentFields.projectId),
      TaskDocumentFields.type: _requiredLegacyCode(
        document,
        TaskDocumentFields.type,
        _taskTypeCodes,
      ),
      TaskDocumentFields.status: status,
      TaskDocumentFields.priority: _nullableLegacyCode(
        document,
        TaskDocumentFields.priority,
        _priorityCodes,
      ),
      TaskDocumentFields.reward: _requiredLegacyCode(
        document,
        TaskDocumentFields.reward,
        _rewardCodes,
      ),
      TaskDocumentFields.difficulty: _requiredLegacyCode(
        document,
        TaskDocumentFields.difficulty,
        _difficultyCodes,
      ),
      TaskDocumentFields.durationMinutes:
          _requiredNullableInt(document, TaskDocumentFields.durationMinutes),
      TaskDocumentFields.scheduledStart: _nullableInstant(
        document,
        TaskDocumentFields.scheduledStart,
      ),
      TaskDocumentFields.scheduledEnd: _nullableInstant(
        document,
        TaskDocumentFields.scheduledEnd,
      ),
      TaskDocumentFields.actualStart:
          _nullableInstant(document, TaskDocumentFields.actualStart),
      TaskDocumentFields.actualEnd:
          _nullableInstant(document, TaskDocumentFields.actualEnd),
      TaskDocumentFields.completedAt:
          _nullableInstant(document, TaskDocumentFields.completedAt),
      TaskDocumentFields.parentTaskId:
          _requiredNullableString(document, TaskDocumentFields.parentTaskId),
      TaskDocumentFields.backlogTags: _requiredLegacyCodeList(
        document,
        TaskDocumentFields.backlogTags,
        _backlogTagCodes,
      ),
      TaskDocumentFields.reminderOverride: _nullableLegacyCode(
        document,
        TaskDocumentFields.reminderOverride,
        _reminderProfileCodes,
      ),
      TaskDocumentFields.stats:
          _requiredMap(document, TaskDocumentFields.stats),
      TaskDocumentFields.backlogEnteredAt: isBacklog ? createdAt : null,
      TaskDocumentFields.backlogEnteredAtProvenance: isBacklog
          ? DocumentMigrationProvenance.approximatedFromCreatedAt
          : null,
    };
  }

  /// Runs the `_projectV0ToV1` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  Map<String, Object?> _projectV0ToV1(
    Map<String, Object?> document,
    List<DocumentMigrationNote> notes,
  ) {
    final metadata = _metadataInstants(document, notes);
    return {
      ..._commonV1Fields(
        id: _requiredString(document, ProjectDocumentFields.id),
        createdAt: metadata.createdAt,
        updatedAt: metadata.updatedAt,
      ),
      ProjectDocumentFields.name:
          _requiredString(document, ProjectDocumentFields.name),
      ProjectDocumentFields.colorKey:
          _requiredString(document, ProjectDocumentFields.colorKey),
      ProjectDocumentFields.defaultPriority: _requiredLegacyCode(
        document,
        ProjectDocumentFields.defaultPriority,
        _priorityCodes,
      ),
      ProjectDocumentFields.defaultReward: _requiredLegacyCode(
        document,
        ProjectDocumentFields.defaultReward,
        _rewardCodes,
      ),
      ProjectDocumentFields.defaultDifficulty: _requiredLegacyCode(
        document,
        ProjectDocumentFields.defaultDifficulty,
        _difficultyCodes,
      ),
      ProjectDocumentFields.defaultReminderProfile: _requiredLegacyCode(
        document,
        ProjectDocumentFields.defaultReminderProfile,
        _reminderProfileCodes,
      ),
      ProjectDocumentFields.defaultDurationMinutes: _requiredNullableInt(
        document,
        ProjectDocumentFields.defaultDurationMinutes,
      ),
      ProjectDocumentFields.archivedAt:
          _optionalNullableInstant(document, ProjectDocumentFields.archivedAt),
    };
  }

  /// Runs the `_lockedBlockV0ToV1` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  Map<String, Object?> _lockedBlockV0ToV1(
    Map<String, Object?> document,
    List<DocumentMigrationNote> notes,
  ) {
    final metadata = _metadataInstants(document, notes);
    return {
      ..._commonV1Fields(
        id: _requiredString(document, LockedBlockDocumentFields.id),
        createdAt: metadata.createdAt,
        updatedAt: metadata.updatedAt,
      ),
      LockedBlockDocumentFields.name:
          _requiredString(document, LockedBlockDocumentFields.name),
      LockedBlockDocumentFields.startTime:
          _requiredWallTime(document, LockedBlockDocumentFields.startTime),
      LockedBlockDocumentFields.endTime:
          _requiredWallTime(document, LockedBlockDocumentFields.endTime),
      LockedBlockDocumentFields.date:
          _requiredNullableCivilDate(document, LockedBlockDocumentFields.date),
      LockedBlockDocumentFields.recurrence:
          _nullableRecurrence(document, LockedBlockDocumentFields.recurrence),
      LockedBlockDocumentFields.hiddenByDefault:
          _requiredBool(document, LockedBlockDocumentFields.hiddenByDefault),
      LockedBlockDocumentFields.projectId: _requiredNullableString(
        document,
        LockedBlockDocumentFields.projectId,
      ),
      LockedBlockDocumentFields.archivedAt: _optionalNullableInstant(
        document,
        LockedBlockDocumentFields.archivedAt,
      ),
    };
  }

  /// Runs the `_lockedBlockOverrideV0ToV1` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  Map<String, Object?> _lockedBlockOverrideV0ToV1(
    Map<String, Object?> document,
    List<DocumentMigrationNote> notes,
  ) {
    final metadata = _metadataInstants(document, notes);
    return {
      ..._commonV1Fields(
        id: _requiredString(document, LockedBlockOverrideDocumentFields.id),
        createdAt: metadata.createdAt,
        updatedAt: metadata.updatedAt,
      ),
      LockedBlockOverrideDocumentFields.lockedBlockId: _requiredNullableString(
        document,
        LockedBlockOverrideDocumentFields.lockedBlockId,
      ),
      LockedBlockOverrideDocumentFields.date: _requiredCivilDate(
        document,
        LockedBlockOverrideDocumentFields.date,
      ),
      LockedBlockOverrideDocumentFields.type: _requiredLegacyCode(
        document,
        LockedBlockOverrideDocumentFields.type,
        _lockedOverrideTypeCodes,
      ),
      LockedBlockOverrideDocumentFields.name: _requiredNullableString(
        document,
        LockedBlockOverrideDocumentFields.name,
      ),
      LockedBlockOverrideDocumentFields.startTime: _requiredNullableWallTime(
        document,
        LockedBlockOverrideDocumentFields.startTime,
      ),
      LockedBlockOverrideDocumentFields.endTime: _requiredNullableWallTime(
        document,
        LockedBlockOverrideDocumentFields.endTime,
      ),
      LockedBlockOverrideDocumentFields.hiddenByDefault: _requiredBool(
        document,
        LockedBlockOverrideDocumentFields.hiddenByDefault,
      ),
      LockedBlockOverrideDocumentFields.projectId: _requiredNullableString(
        document,
        LockedBlockOverrideDocumentFields.projectId,
      ),
    };
  }

  /// Runs the `_commonV1Fields` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  Map<String, Object?> _commonV1Fields({
    required String id,
    required String createdAt,
    required String updatedAt,
  }) {
    return {
      DocumentFields.schemaVersion: v1SchemaVersion,
      DocumentFields.id: id,
      DocumentFields.ownerId: ownerId,
      DocumentFields.revision: 1,
      DocumentFields.createdAt: createdAt,
      DocumentFields.updatedAt: updatedAt,
    };
  }

  /// Runs the `_metadataInstants` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  _LegacyMetadataInstants _metadataInstants(
    Map<String, Object?> document,
    List<DocumentMigrationNote> notes,
  ) {
    final createdAt =
        _optionalNullableInstant(document, DocumentFields.createdAt);
    final updatedAt =
        _optionalNullableInstant(document, DocumentFields.updatedAt);
    if (createdAt == null || updatedAt == null) {
      notes.add(
        const DocumentMigrationNote(
          code: DocumentMigrationNoteCode.synthesizedMetadataTimestamp,
          detailCode: 'migratedAt',
        ),
      );
    }
    final fallback = PersistenceDateTimeConvention.toStoredString(migratedAt);
    return _LegacyMetadataInstants(
      createdAt: createdAt ?? fallback,
      updatedAt: updatedAt ?? createdAt ?? fallback,
    );
  }
}
