// V1 management queries and commands.
//
// These use cases cover non-scheduling surfaces such as backlog views, project
// defaults, locked-time definitions, owner settings, and one-time notice
// acknowledgement. They stay UI-independent and use the same atomic
// application unit-of-work boundary as task commands.

import 'application_layer.dart';
import 'backlog.dart';
import 'locked_time.dart';
import 'models.dart';
import 'project_statistics.dart';
import 'time_contracts.dart';

/// Stable management command identifiers.
enum ApplicationManagementCommandCode {
  createProject,
  updateProject,
  archiveProject,
  createLockedBlock,
  updateLockedBlock,
  archiveLockedBlock,
  addLockedBlockOverride,
  removeLockedBlockOccurrence,
  replaceLockedBlockOccurrence,
  updateOwnerSettings,
  acknowledgeNotice,
}

/// Typed warning codes for settings changes that reinterpret local dates/times.
enum OwnerSettingsWarningCode {
  timeZoneChanged,
  planningWindowChanged,
}

/// Backlog query request.
class GetBacklogRequest {
  const GetBacklogRequest({
    required this.context,
    this.filter,
    this.sortKey = BacklogSortKey.priority,
  });

  /// Read context containing owner scope and read instant.
  final ApplicationOperationContext context;

  /// Optional user-facing backlog filter.
  final BacklogFilter? filter;

  /// User-facing sort order.
  final BacklogSortKey sortKey;
}

/// One backlog row with its configured staleness marker.
class BacklogItemReadModel {
  const BacklogItemReadModel({
    required this.task,
    required this.stalenessMarker,
  });

  /// Source backlog task.
  final Task task;

  /// Green/blue/purple marker using owner settings.
  final BacklogStalenessMarker stalenessMarker;
}

/// Query result for the backlog surface.
class BacklogQueryResult {
  BacklogQueryResult({
    required this.ownerId,
    required this.settings,
    required List<BacklogItemReadModel> items,
  }) : items = List<BacklogItemReadModel>.unmodifiable(items);

  /// Authorization-neutral owner scope.
  final String ownerId;

  /// Effective owner settings used by the query.
  final OwnerSettings settings;

  /// Filtered and sorted backlog rows.
  final List<BacklogItemReadModel> items;
}

/// Query request for project defaults and learned suggestions.
class GetProjectDefaultsRequest {
  const GetProjectDefaultsRequest({
    required this.context,
    this.includeArchived = false,
  });

  /// Read context containing owner scope and read instant.
  final ApplicationOperationContext context;

  /// Whether archived projects should be included.
  final bool includeArchived;
}

/// Project defaults query result.
class ProjectDefaultsQueryResult {
  ProjectDefaultsQueryResult({
    required this.ownerId,
    required List<ProjectDefaultResolution> projects,
  }) : projects = List<ProjectDefaultResolution>.unmodifiable(projects);

  /// Authorization-neutral owner scope.
  final String ownerId;

  /// Configured defaults plus separately surfaced learned suggestions.
  final List<ProjectDefaultResolution> projects;
}

/// Draft used when creating a project.
class ProjectProfileDraft {
  const ProjectProfileDraft({
    this.id,
    required this.name,
    required this.colorKey,
    this.defaultPriority = PriorityLevel.medium,
    this.defaultReward = RewardLevel.notSet,
    this.defaultDifficulty = DifficultyLevel.notSet,
    this.defaultReminderProfile = ReminderProfile.gentle,
    this.defaultDurationMinutes,
  });

  final String? id;
  final String name;
  final String colorKey;
  final PriorityLevel defaultPriority;
  final RewardLevel defaultReward;
  final DifficultyLevel defaultDifficulty;
  final ReminderProfile defaultReminderProfile;
  final int? defaultDurationMinutes;
}

/// Patch used when updating configured project defaults.
class ProjectProfileUpdate {
  const ProjectProfileUpdate({
    this.name,
    this.colorKey,
    this.defaultPriority,
    this.defaultReward,
    this.defaultDifficulty,
    this.defaultReminderProfile,
    this.defaultDurationMinutes,
    this.clearDefaultDuration = false,
  });

  final String? name;
  final String? colorKey;
  final PriorityLevel? defaultPriority;
  final RewardLevel? defaultReward;
  final DifficultyLevel? defaultDifficulty;
  final ReminderProfile? defaultReminderProfile;
  final int? defaultDurationMinutes;
  final bool clearDefaultDuration;
}

/// Draft used when creating a locked block.
class LockedBlockDraft {
  const LockedBlockDraft({
    this.id,
    required this.name,
    required this.startTime,
    required this.endTime,
    this.date,
    this.recurrence,
    this.hiddenByDefault = true,
    this.projectId,
  });

  final String? id;
  final String name;
  final ClockTime startTime;
  final ClockTime endTime;
  final CivilDate? date;
  final LockedBlockRecurrence? recurrence;
  final bool hiddenByDefault;
  final String? projectId;
}

/// Patch used when updating a locked block definition.
class LockedBlockUpdate {
  const LockedBlockUpdate({
    this.name,
    this.startTime,
    this.endTime,
    this.date,
    this.recurrence,
    this.hiddenByDefault,
    this.projectId,
  });

  final String? name;
  final ClockTime? startTime;
  final ClockTime? endTime;
  final CivilDate? date;
  final LockedBlockRecurrence? recurrence;
  final bool? hiddenByDefault;
  final String? projectId;
}

/// Result for owner settings updates.
class OwnerSettingsUpdateResult {
  OwnerSettingsUpdateResult({
    required this.settings,
    Iterable<OwnerSettingsWarningCode> warnings =
        const <OwnerSettingsWarningCode>[],
  }) : warnings = List<OwnerSettingsWarningCode>.unmodifiable(warnings);

  /// Persisted owner settings.
  final OwnerSettings settings;

  /// Typed warnings for changes that may need migration/UI confirmation.
  final List<OwnerSettingsWarningCode> warnings;
}

/// Patch used when updating owner settings.
class OwnerSettingsUpdate {
  const OwnerSettingsUpdate({
    this.timeZoneId,
    this.dayStart,
    this.dayEnd,
    this.compactModeEnabled,
    this.backlogStalenessSettings,
  });

  final String? timeZoneId;
  final WallTime? dayStart;
  final WallTime? dayEnd;
  final bool? compactModeEnabled;
  final BacklogStalenessSettings? backlogStalenessSettings;
}

/// Result for notice acknowledgement commands.
class NoticeAcknowledgementResult {
  const NoticeAcknowledgementResult({
    required this.record,
    required this.alreadyAcknowledged,
  });

  /// Persisted acknowledgement record.
  final NoticeAcknowledgementRecord record;

  /// Whether the notice had already been acknowledged before this command.
  final bool alreadyAcknowledged;
}

/// V1 management facade used by future Flutter state management.
class V1ApplicationManagementUseCases {
  const V1ApplicationManagementUseCases({
    required this.applicationStore,
    this.projectSuggestionService = const ProjectSuggestionService(),
  });

  /// Atomic application boundary.
  final ApplicationUnitOfWork applicationStore;

  /// Learned project suggestion policy.
  final ProjectSuggestionService projectSuggestionService;

  /// Load backlog candidates through the repository status filter, then apply
  /// existing filter/sort/staleness policies in memory.
  Future<ApplicationResult<BacklogQueryResult>> getBacklog(
    GetBacklogRequest request,
  ) {
    return applicationStore.read(
      action: (repositories) async {
        final ownerId = request.context.ownerTimeZone.ownerId;
        final settings = await _ownerSettingsOrDefault(
          repositories,
          request.context.ownerTimeZone,
        );
        final candidates = await repositories.tasks.findByStatus(
          TaskStatus.backlog,
        );
        final filtered = request.filter == null
            ? candidates
            : BacklogView(
                tasks: candidates,
                now: request.context.now,
                stalenessSettings: settings.backlogStalenessSettings,
              ).filter(request.filter!);
        final view = BacklogView(
          tasks: filtered,
          now: request.context.now,
          stalenessSettings: settings.backlogStalenessSettings,
        );
        final sorted = view.sorted(request.sortKey);

        return ApplicationResult.success(
          BacklogQueryResult(
            ownerId: ownerId,
            settings: settings,
            items: [
              for (final task in sorted)
                BacklogItemReadModel(
                  task: task,
                  stalenessMarker: view.stalenessMarkerFor(task),
                ),
            ],
          ),
        );
      },
    );
  }

  /// Load configured project defaults with learned suggestions kept separate.
  Future<ApplicationResult<ProjectDefaultsQueryResult>> getProjectDefaults(
    GetProjectDefaultsRequest request,
  ) {
    return applicationStore.read(
      action: (repositories) async {
        final projects = (await repositories.projects.findByOwner(
          ownerId: request.context.ownerTimeZone.ownerId,
          includeArchived: request.includeArchived,
        ))
            .items;
        final resolutions = <ProjectDefaultResolution>[];
        final sortedProjects = [...projects]
          ..sort((a, b) => a.name.compareTo(b.name));

        for (final project in sortedProjects) {
          if (project.isArchived && !request.includeArchived) {
            continue;
          }
          final statistics =
              await repositories.projectStatistics.findByProjectId(project.id);
          resolutions.add(
            projectSuggestionService.resolveDefaults(
              project: project,
              statistics:
                  statistics ?? ProjectStatistics(projectId: project.id),
            ),
          );
        }

        return ApplicationResult.success(
          ProjectDefaultsQueryResult(
            ownerId: request.context.ownerTimeZone.ownerId,
            projects: resolutions,
          ),
        );
      },
    );
  }

  Future<ApplicationResult<ProjectProfile>> createProject({
    required ApplicationOperationContext context,
    required ProjectProfileDraft draft,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.createProject.name,
      action: (repositories) async {
        final id = draft.id ?? context.idGenerator.nextId();
        final existing = await repositories.projects.findById(id);
        if (existing != null) {
          return _failure(
            ApplicationFailureCode.conflict,
            entityId: id,
            detailCode: 'projectAlreadyExists',
          );
        }
        final project = ProjectProfile(
          id: id,
          name: draft.name,
          colorKey: draft.colorKey,
          defaultPriority: draft.defaultPriority,
          defaultReward: draft.defaultReward,
          defaultDifficulty: draft.defaultDifficulty,
          defaultReminderProfile: draft.defaultReminderProfile,
          defaultDurationMinutes: draft.defaultDurationMinutes,
        );
        await repositories.projects.save(project);
        return ApplicationResult.success(project);
      },
    );
  }

  Future<ApplicationResult<ProjectProfile>> updateProject({
    required ApplicationOperationContext context,
    required String projectId,
    required ProjectProfileUpdate update,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.updateProject.name,
      action: (repositories) async {
        final project = await _requireProject(repositories, projectId);
        final updated = project.copyWith(
          name: update.name,
          colorKey: update.colorKey,
          defaultPriority: update.defaultPriority,
          defaultReward: update.defaultReward,
          defaultDifficulty: update.defaultDifficulty,
          defaultReminderProfile: update.defaultReminderProfile,
          defaultDurationMinutes: update.defaultDurationMinutes,
          clearDefaultDuration: update.clearDefaultDuration,
        );
        await repositories.projects.save(updated);
        return ApplicationResult.success(updated);
      },
    );
  }

  Future<ApplicationResult<ProjectProfile>> archiveProject({
    required ApplicationOperationContext context,
    required String projectId,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.archiveProject.name,
      action: (repositories) async {
        final project = await _requireProject(repositories, projectId);
        final archived = project.copyWith(archivedAt: context.now);
        await repositories.projects.save(archived);
        return ApplicationResult.success(archived);
      },
    );
  }

  Future<ApplicationResult<LockedBlock>> createLockedBlock({
    required ApplicationOperationContext context,
    required LockedBlockDraft draft,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.createLockedBlock.name,
      action: (repositories) async {
        final id = draft.id ?? context.idGenerator.nextId();
        final existing = await repositories.lockedBlocks.findBlockById(id);
        if (existing != null) {
          return _failure(
            ApplicationFailureCode.conflict,
            entityId: id,
            detailCode: 'lockedBlockAlreadyExists',
          );
        }
        final block = LockedBlock(
          id: id,
          name: draft.name,
          startTime: draft.startTime,
          endTime: draft.endTime,
          date: draft.date,
          recurrence: draft.recurrence,
          hiddenByDefault: draft.hiddenByDefault,
          projectId: draft.projectId,
          createdAt: context.now,
          updatedAt: context.now,
        );
        await repositories.lockedBlocks.saveBlock(block);
        return ApplicationResult.success(block);
      },
    );
  }

  Future<ApplicationResult<LockedBlock>> updateLockedBlock({
    required ApplicationOperationContext context,
    required String lockedBlockId,
    required LockedBlockUpdate update,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.updateLockedBlock.name,
      action: (repositories) async {
        final block = await _requireLockedBlock(repositories, lockedBlockId);
        final updated = block.copyWith(
          name: update.name,
          startTime: update.startTime,
          endTime: update.endTime,
          date: update.date,
          recurrence: update.recurrence,
          hiddenByDefault: update.hiddenByDefault,
          projectId: update.projectId,
          updatedAt: context.now,
        );
        await repositories.lockedBlocks.saveBlock(updated);
        return ApplicationResult.success(updated);
      },
    );
  }

  Future<ApplicationResult<LockedBlock>> archiveLockedBlock({
    required ApplicationOperationContext context,
    required String lockedBlockId,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.archiveLockedBlock.name,
      action: (repositories) async {
        final block = await _requireLockedBlock(repositories, lockedBlockId);
        final archived = block.copyWith(
          updatedAt: context.now,
          archivedAt: context.now,
        );
        await repositories.lockedBlocks.saveBlock(archived);
        return ApplicationResult.success(archived);
      },
    );
  }

  Future<ApplicationResult<LockedBlockOverride>> addOneDayLockedOverride({
    required ApplicationOperationContext context,
    required CivilDate date,
    required String name,
    required ClockTime startTime,
    required ClockTime endTime,
    bool hiddenByDefault = true,
    String? projectId,
  }) {
    return applicationStore.run(
      context: context,
      operationName:
          ApplicationManagementCommandCode.addLockedBlockOverride.name,
      action: (repositories) async {
        final override = LockedBlockOverride.add(
          id: context.idGenerator.nextId(),
          date: date,
          name: name,
          startTime: startTime,
          endTime: endTime,
          hiddenByDefault: hiddenByDefault,
          projectId: projectId,
          createdAt: context.now,
        );
        await repositories.lockedBlocks.saveOverride(override);
        return ApplicationResult.success(override);
      },
    );
  }

  Future<ApplicationResult<LockedBlockOverride>> removeOneDayLockedOccurrence({
    required ApplicationOperationContext context,
    required String lockedBlockId,
    required CivilDate date,
  }) {
    return applicationStore.run(
      context: context,
      operationName:
          ApplicationManagementCommandCode.removeLockedBlockOccurrence.name,
      action: (repositories) async {
        await _requireLockedBlock(repositories, lockedBlockId);
        final override = LockedBlockOverride.remove(
          id: context.idGenerator.nextId(),
          lockedBlockId: lockedBlockId,
          date: date,
          createdAt: context.now,
        );
        await repositories.lockedBlocks.saveOverride(override);
        return ApplicationResult.success(override);
      },
    );
  }

  Future<ApplicationResult<LockedBlockOverride>> replaceOneDayLockedOccurrence({
    required ApplicationOperationContext context,
    required String lockedBlockId,
    required CivilDate date,
    required ClockTime startTime,
    required ClockTime endTime,
    String? name,
    bool hiddenByDefault = true,
    String? projectId,
  }) {
    return applicationStore.run(
      context: context,
      operationName:
          ApplicationManagementCommandCode.replaceLockedBlockOccurrence.name,
      action: (repositories) async {
        await _requireLockedBlock(repositories, lockedBlockId);
        final override = LockedBlockOverride.replace(
          id: context.idGenerator.nextId(),
          lockedBlockId: lockedBlockId,
          date: date,
          startTime: startTime,
          endTime: endTime,
          name: name,
          hiddenByDefault: hiddenByDefault,
          projectId: projectId,
          createdAt: context.now,
        );
        await repositories.lockedBlocks.saveOverride(override);
        return ApplicationResult.success(override);
      },
    );
  }

  Future<ApplicationResult<OwnerSettingsUpdateResult>> updateOwnerSettings({
    required ApplicationOperationContext context,
    required OwnerSettingsUpdate update,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.updateOwnerSettings.name,
      action: (repositories) async {
        final current = await _ownerSettingsOrDefault(
          repositories,
          context.ownerTimeZone,
        );
        final stalenessSettings = update.backlogStalenessSettings;
        if (stalenessSettings != null &&
            (stalenessSettings.greenMaxAge.isNegative ||
                stalenessSettings.blueMaxAge.isNegative ||
                stalenessSettings.blueMaxAge < stalenessSettings.greenMaxAge)) {
          return _failure(
            ApplicationFailureCode.validation,
            detailCode: 'invalidBacklogStalenessThresholds',
          );
        }
        final updated = current.copyWith(
          timeZoneId: update.timeZoneId,
          dayStart: update.dayStart,
          dayEnd: update.dayEnd,
          compactModeEnabled: update.compactModeEnabled,
          backlogStalenessSettings: update.backlogStalenessSettings,
        );
        final warnings = <OwnerSettingsWarningCode>[
          if (update.timeZoneId != null &&
              update.timeZoneId != current.timeZoneId)
            OwnerSettingsWarningCode.timeZoneChanged,
          if ((update.dayStart != null &&
                  update.dayStart != current.dayStart) ||
              (update.dayEnd != null && update.dayEnd != current.dayEnd))
            OwnerSettingsWarningCode.planningWindowChanged,
        ];
        await repositories.ownerSettings.save(updated);
        return ApplicationResult.success(
          OwnerSettingsUpdateResult(settings: updated, warnings: warnings),
        );
      },
    );
  }

  Future<ApplicationResult<NoticeAcknowledgementResult>> acknowledgeNotice({
    required ApplicationOperationContext context,
    required String noticeId,
  }) {
    return applicationStore.run(
      context: context,
      operationName: ApplicationManagementCommandCode.acknowledgeNotice.name,
      action: (repositories) async {
        final ownerId = context.ownerTimeZone.ownerId;
        final parsed = _parseNoticeId(noticeId);
        if (parsed == null) {
          return _failure(
            ApplicationFailureCode.validation,
            entityId: noticeId,
            detailCode: 'invalidNoticeId',
          );
        }
        final snapshot =
            await repositories.schedulingSnapshots.findById(parsed.snapshotId);
        if (snapshot == null || parsed.noticeIndex >= snapshot.notices.length) {
          return _failure(
            ApplicationFailureCode.notFound,
            entityId: noticeId,
            detailCode: 'noticeNotFound',
          );
        }
        final existing = await repositories.noticeAcknowledgements.findById(
          ownerId: ownerId,
          noticeId: noticeId,
        );
        if (existing != null) {
          return ApplicationResult.success(
            NoticeAcknowledgementResult(
              record: existing,
              alreadyAcknowledged: true,
            ),
          );
        }

        final record = NoticeAcknowledgementRecord(
          noticeId: noticeId,
          ownerId: ownerId,
          acknowledgedAt: context.now,
        );
        await repositories.noticeAcknowledgements.save(record);
        return ApplicationResult.success(
          NoticeAcknowledgementResult(
            record: record,
            alreadyAcknowledged: false,
          ),
        );
      },
    );
  }

  Future<ProjectProfile> _requireProject(
    ApplicationUnitOfWorkRepositories repositories,
    String projectId,
  ) async {
    final project = await repositories.projects.findById(projectId);
    if (project == null) {
      throw ApplicationFailureException(
        ApplicationFailure(
          code: ApplicationFailureCode.notFound,
          entityId: projectId,
          detailCode: 'projectNotFound',
        ),
      );
    }
    return project;
  }

  Future<LockedBlock> _requireLockedBlock(
    ApplicationUnitOfWorkRepositories repositories,
    String lockedBlockId,
  ) async {
    final block = await repositories.lockedBlocks.findBlockById(lockedBlockId);
    if (block == null) {
      throw ApplicationFailureException(
        ApplicationFailure(
          code: ApplicationFailureCode.notFound,
          entityId: lockedBlockId,
          detailCode: 'lockedBlockNotFound',
        ),
      );
    }
    return block;
  }
}

Future<OwnerSettings> _ownerSettingsOrDefault(
  ApplicationUnitOfWorkRepositories repositories,
  OwnerTimeZoneContext ownerTimeZone,
) async {
  return await repositories.ownerSettings
          .findByOwnerId(ownerTimeZone.ownerId) ??
      OwnerSettings(
        ownerId: ownerTimeZone.ownerId,
        timeZoneId: ownerTimeZone.timeZoneId,
      );
}

ApplicationResult<T> _failure<T>(
  ApplicationFailureCode code, {
  String? entityId,
  String? detailCode,
}) {
  return ApplicationResult.failure(
    ApplicationFailure(
      code: code,
      entityId: entityId,
      detailCode: detailCode,
    ),
  );
}

_ParsedNoticeId? _parseNoticeId(String noticeId) {
  final separator = noticeId.lastIndexOf(':');
  if (separator <= 0 || separator == noticeId.length - 1) {
    return null;
  }
  final noticeIndex = int.tryParse(noticeId.substring(separator + 1));
  if (noticeIndex == null || noticeIndex < 0) {
    return null;
  }
  return _ParsedNoticeId(
    snapshotId: noticeId.substring(0, separator),
    noticeIndex: noticeIndex,
  );
}

class _ParsedNoticeId {
  const _ParsedNoticeId({
    required this.snapshotId,
    required this.noticeIndex,
  });

  final String snapshotId;
  final int noticeIndex;
}
