import type {
  AuditRecordId,
  BusinessEvent,
  CorrelationId,
  EventId,
  FactoryRunId,
  GoalId,
  MutationContext,
  OutboxMessageId,
  PlanId,
  PlanRevisionId,
  ProjectId,
  ProjectVisionVersionId,
  TaskId,
} from "@awp/contracts";
import type {
  AgentRun,
  Attempt,
  ChangeSet,
  FactoryRun,
  Goal,
  Plan,
  PlanRevision,
  Project,
  ProjectVisionVersion,
  PlanningSession,
  ProjectPlanningDefaults,
  Review,
  ReviewFinding,
  Task,
  VerificationEvidence,
  Workspace,
} from "@awp/domain";
import { validateTaskDependencies, unsatisfiedDependencies } from "@awp/domain";
import type {
  ExecutionDispatchRequest,
  ExecutionDispatcher,
  ExecutionSelection,
} from "./execution.js";
import type { Clock, IdGenerator } from "./ports/runtime.js";
import type { ApplicationTransaction, UnitOfWork } from "./ports/repositories.js";
import type { ProjectExecutionConfigurationResolver } from "./configuration.js";

export interface CreateProjectInput {
  readonly name: string;
  readonly repositoryUrl: string;
  readonly requiredChecks?: readonly string[];
  readonly context: MutationContext;
}
export interface CreateGoalInput {
  readonly projectId: ProjectId;
  readonly title: string;
  readonly successCriteria: readonly string[];
  readonly context: MutationContext;
}
export interface CreatePlanInput {
  readonly projectId: ProjectId;
  readonly title: string;
  readonly taskTitles: readonly string[];
  readonly context: MutationContext;
}

export interface ProjectHierarchy {
  readonly project: Project;
  readonly vision?: ProjectVisionVersion;
  readonly goals: readonly Goal[];
  readonly plans: readonly Plan[];
  readonly planRevisions: readonly PlanRevision[];
  readonly planningSessions: readonly PlanningSession[];
  readonly planningDefaults?: ProjectPlanningDefaults;
  readonly tasks: readonly Task[];
  readonly factoryRuns: readonly FactoryRun[];
  readonly agentRuns: readonly AgentRun[];
  readonly attempts: readonly Attempt[];
  readonly workspaces: readonly Workspace[];
  readonly changeSets: readonly ChangeSet[];
  readonly reviews: readonly Review[];
  readonly verificationEvidence: readonly VerificationEvidence[];
  readonly reviewFindings: readonly ReviewFinding[];
  readonly events: readonly BusinessEvent[];
}

function requireText(value: string, label: string): string {
  const normalized = value.trim();
  if (!normalized) throw new Error(`${label} is required`);
  return normalized;
}

function validateRepositoryUrl(value: string): string {
  const normalized = requireText(value, "Repository URL");
  let parsed: URL;
  try {
    parsed = new URL(normalized);
  } catch {
    throw new Error("Repository URL must be an absolute URL");
  }
  if (!["https:", "ssh:"].includes(parsed.protocol)) {
    throw new Error("Repository URL must use https or ssh");
  }
  return normalized;
}

function normalizeRequiredChecks(values: readonly string[]): readonly string[] {
  const checks = values.map((value) => requireText(value, "Required check"));
  return [...new Set(checks)].sort((a, b) => a.localeCompare(b));
}

export class I1LifecycleService {
  constructor(
    private readonly uow: UnitOfWork,
    private readonly ids: IdGenerator,
    private readonly clock: Clock,
    private readonly executionDispatcher?: ExecutionDispatcher,
    private readonly configuration?: ProjectExecutionConfigurationResolver,
  ) {}

  async createProject(input: CreateProjectInput): Promise<Project> {
    const project: Project = {
      id: this.ids.next<ProjectId>(),
      name: requireText(input.name, "Project name"),
      repositoryUrl: validateRepositoryUrl(input.repositoryUrl),
      requiredChecks: normalizeRequiredChecks(input.requiredChecks ?? []),
      status: "active",
      revision: 1,
    };
    return this.uow.transaction(async (tx) => {
      await tx.projects.insert(project);
      await this.record(tx, input.context, "ProjectCreated", "Project", project.id, 1, project.id, {
        name: project.name,
        repositoryUrl: project.repositoryUrl,
        requiredChecks: project.requiredChecks,
      });
      return project;
    });
  }

  async setProjectRequiredChecks(
    projectId: ProjectId,
    requiredChecks: readonly string[],
    context: MutationContext,
  ): Promise<Project> {
    const normalized = normalizeRequiredChecks(requiredChecks);
    return this.uow.transaction(async (tx) => {
      const project = await this.requireProject(tx, projectId);
      if (
        project.requiredChecks.length === normalized.length &&
        project.requiredChecks.every((value, index) => value === normalized[index])
      ) {
        return project;
      }
      const next: Project = {
        ...project,
        requiredChecks: normalized,
        revision: project.revision + 1,
      };
      await tx.projects.update(next);
      await this.record(
        tx,
        context,
        "ProjectVerificationPolicyUpdated",
        "Project",
        project.id,
        next.revision,
        project.id,
        { requiredChecks: normalized },
      );
      return next;
    });
  }

  async setProjectVision(
    projectId: ProjectId,
    summary: string,
    context: MutationContext,
  ): Promise<ProjectVisionVersion> {
    return this.uow.transaction(async (tx) => {
      const project = await this.requireProject(tx, projectId);
      const previous = await tx.projectVisions.getCurrent(projectId);
      const vision: ProjectVisionVersion = {
        id: this.ids.next<ProjectVisionVersionId>(),
        projectId,
        sequence: (previous?.sequence ?? 0) + 1,
        summary: requireText(summary, "ProjectVision"),
        ...(previous ? { supersedesId: previous.id } : {}),
      };
      await tx.projectVisions.insert(vision);
      await this.record(
        tx,
        context,
        "ProjectVisionVersionCreated",
        "ProjectVision",
        vision.id,
        vision.sequence,
        project.id,
        {
          summary: vision.summary,
          sequence: vision.sequence,
        },
      );
      return vision;
    });
  }

  async createGoal(input: CreateGoalInput): Promise<Goal> {
    if (
      input.successCriteria.length === 0 ||
      input.successCriteria.some((criterion) => !criterion.trim())
    ) {
      throw new Error("Goal requires at least one written success criterion");
    }
    const goal: Goal = {
      id: this.ids.next<GoalId>(),
      projectId: input.projectId,
      title: requireText(input.title, "Goal title"),
      status: "active",
      successCriteria: input.successCriteria.map((criterion) => criterion.trim()),
      revision: 1,
    };
    return this.uow.transaction(async (tx) => {
      await this.requireProject(tx, input.projectId);
      await tx.goals.insert(goal);
      await this.record(tx, input.context, "GoalCreated", "Goal", goal.id, 1, goal.projectId, {
        title: goal.title,
        successCriteria: goal.successCriteria,
      });
      return goal;
    });
  }

  async createPlan(
    input: CreatePlanInput,
  ): Promise<{ plan: Plan; revision: PlanRevision; tasks: readonly Task[] }> {
    if (input.taskTitles.length < 3) throw new Error("I1 Plan requires at least three Tasks");
    const titles = input.taskTitles.map((title) => requireText(title, "Task title"));
    return this.uow.transaction(async (tx) => {
      await this.requireProject(tx, input.projectId);
      const goals = await tx.goals.listByProject(input.projectId);
      const vision = await tx.projectVisions.getCurrent(input.projectId);
      const plan: Plan = {
        id: this.ids.next<PlanId>(),
        projectId: input.projectId,
        title: requireText(input.title, "Plan title"),
        status: "draft",
        revision: 1,
      };
      const revision: PlanRevision = {
        id: this.ids.next<PlanRevisionId>(),
        planId: plan.id,
        projectId: input.projectId,
        sequence: 1,
        title: plan.title,
        goalIds: goals.map((goal) => goal.id),
        ...(vision ? { projectVisionVersionId: vision.id } : {}),
      };
      const tasks: Task[] = titles.map((title, position) => ({
        id: this.ids.next<TaskId>(),
        projectId: input.projectId,
        planRevisionId: revision.id,
        title,
        position,
        status: "planned",
        dependencyIds: [],
        goalIds: revision.goalIds,
        revision: 1,
      }));
      await tx.plans.insert(plan);
      await tx.planRevisions.insert(revision);
      for (const task of tasks) await tx.tasks.insert(task);
      await this.record(tx, input.context, "PlanCreated", "Plan", plan.id, 1, input.projectId, {
        title: plan.title,
        taskIds: tasks.map((task) => task.id),
      });
      return { plan, revision, tasks };
    });
  }

  async addDependency(
    projectId: ProjectId,
    taskId: TaskId,
    prerequisiteTaskId: TaskId,
    context: MutationContext,
  ): Promise<Task> {
    return this.uow.transaction(async (tx) => {
      await this.requireProject(tx, projectId);
      const tasks = await tx.tasks.listByProject(projectId);
      const target = tasks.find((task) => task.id === taskId);
      if (!target) throw new Error(`Unknown Task ${taskId}`);
      if (!tasks.some((task) => task.id === prerequisiteTaskId))
        throw new Error(`Unknown prerequisite Task ${prerequisiteTaskId}`);
      const updated: Task = {
        ...target,
        dependencyIds: [...new Set([...target.dependencyIds, prerequisiteTaskId])],
        status: "blocked",
        revision: target.revision + 1,
      };
      validateTaskDependencies(tasks.map((task) => (task.id === taskId ? updated : task)));
      await tx.tasks.update(updated);
      await this.record(
        tx,
        context,
        "TaskDependencyAdded",
        "Task",
        taskId,
        updated.revision,
        projectId,
        {
          prerequisiteTaskId,
        },
      );
      return updated;
    });
  }

  async approvePlan(
    projectId: ProjectId,
    planId: PlanId,
    context: MutationContext,
    selection?: ExecutionSelection,
  ): Promise<Plan> {
    const approved = await this.uow.transaction(async (tx) => {
      await this.requireProject(tx, projectId);
      const plan = await tx.plans.getById(planId);
      if (!plan || plan.projectId !== projectId) throw new Error(`Unknown Plan ${planId}`);
      const revision = await tx.planRevisions.getCurrent(planId);
      if (!revision) throw new Error(`Plan ${planId} has no revision`);
      const existingRuns = (await tx.factoryRuns.listByProject(projectId)).filter(
        (run) => run.planRevisionId === revision.id,
      );
      if (plan.status === "approved" || plan.status === "completed") {
        if (existingRuns.length !== 1) {
          throw new Error(
            `Approved PlanRevision ${String(revision.id)} must own exactly one FactoryRun`,
          );
        }
        const [existingRun] = existingRuns;
        if (
          selection?.accountId !== undefined &&
          existingRun?.accountId !== undefined &&
          selection.accountId !== existingRun.accountId
        ) {
          throw new Error("Approved Plan replay cannot change FactoryRun account provenance");
        }
        if (
          selection?.model !== undefined &&
          existingRun?.model !== undefined &&
          selection.model !== existingRun.model
        ) {
          throw new Error("Approved Plan replay cannot change FactoryRun model provenance");
        }
        return plan;
      }
      if (plan.status !== "draft" && plan.status !== "ready") {
        throw new Error(`Plan ${String(plan.id)} is not approvable from status ${plan.status}`);
      }
      if (existingRuns.length !== 0) {
        throw new Error(`Unapproved PlanRevision ${String(revision.id)} already owns a FactoryRun`);
      }
      const executionConfiguration = this.configuration
        ? await this.configuration.resolveProjectExecutionInTransaction(tx, projectId)
        : { enabled: true as const };
      if (!executionConfiguration.enabled) {
        throw new Error("Project execution is disabled by configuration");
      }
      const effectiveModel = selection?.model ?? executionConfiguration.defaultModel;
      const projectTasks = await tx.tasks.listByProject(projectId);
      const planTasks = projectTasks.filter((task) => task.planRevisionId === revision.id);
      validateTaskDependencies(projectTasks);
      const readiness = planTasks.map((task) => ({
        task,
        unsatisfied: unsatisfiedDependencies(task, projectTasks),
      }));
      const eligibleTasks = readiness
        .filter(({ unsatisfied }) => unsatisfied.length === 0)
        .map(({ task }) => task)
        .sort((left, right) => (left.position ?? 0) - (right.position ?? 0));
      const eligibleIds = new Set(eligibleTasks.map((task) => task.id));
      for (const { task, unsatisfied } of readiness) {
        const nextTask: Task = {
          ...task,
          status: eligibleIds.has(task.id)
            ? "dispatched"
            : unsatisfied.length === 0
              ? "ready"
              : "blocked",
          revision: task.revision + 1,
        };
        await tx.tasks.update(nextTask);
        if (nextTask.status === "dispatched") {
          await this.record(
            tx,
            context,
            "TaskDispatched",
            "Task",
            nextTask.id,
            nextTask.revision,
            projectId,
            { planId, planRevisionId: revision.id },
          );
        }
      }
      if (eligibleTasks.length === 0) {
        throw new Error("Approved PlanRevision has no dependency-legal Task to dispatch");
      }
      const factoryRun: FactoryRun = {
        id: this.ids.next<FactoryRunId>(),
        projectId,
        planRevisionId: revision.id,
        ...(eligibleTasks.length === 1 ? { taskId: eligibleTasks[0]!.id } : {}),
        ...(selection?.accountId === undefined ? {} : { accountId: selection.accountId }),
        ...(effectiveModel === undefined ? {} : { model: effectiveModel }),
        status: "queued",
        reason: "Automatic dependency-legal dispatch after Plan approval",
        revision: 1,
      };
      await tx.factoryRuns.insert(factoryRun);
      await this.record(
        tx,
        context,
        "FactoryRunQueued",
        "FactoryRun",
        factoryRun.id,
        1,
        projectId,
        {
          planId,
          planRevisionId: revision.id,
          taskIds: eligibleTasks.map((task) => task.id),
          accountSelectionPersisted: factoryRun.accountId !== undefined,
          modelSelectionPersisted: factoryRun.model !== undefined,
        },
      );
      const next: Plan = { ...plan, status: "approved", revision: plan.revision + 1 };
      await tx.plans.update(next);
      await this.record(tx, context, "PlanApproved", "Plan", plan.id, next.revision, projectId, {
        planRevisionId: revision.id,
      });
      return next;
    });
    await this.reconcileDispatchedWork(projectId, context);
    return approved;
  }

  async reconcileDispatchedWork(projectId: ProjectId, context: MutationContext): Promise<number> {
    if (!this.executionDispatcher) return 0;
    const requests = await this.uow.transaction(async (tx) => {
      await this.requireProject(tx, projectId);
      const [runs, tasks] = await Promise.all([
        tx.factoryRuns.listByProject(projectId),
        tx.tasks.listByProject(projectId),
      ]);
      const byRevision = new Map<PlanRevisionId, FactoryRun[]>();
      for (const run of runs) {
        const current = byRevision.get(run.planRevisionId) ?? [];
        current.push(run);
        byRevision.set(run.planRevisionId, current);
      }
      const output: ExecutionDispatchRequest[] = [];
      for (const task of tasks) {
        if (task.status !== "dispatched") continue;
        const matching = byRevision.get(task.planRevisionId) ?? [];
        if (matching.length !== 1) {
          throw new Error(
            `Dispatched Task ${String(task.id)} must belong to exactly one FactoryRun`,
          );
        }
        const factoryRun = matching[0]!;
        if (["completed", "cancelled", "failed"].includes(factoryRun.status)) {
          throw new Error(
            `Terminal FactoryRun ${String(factoryRun.id)} cannot retain dispatched Task ${String(task.id)}`,
          );
        }
        output.push({
          factoryRun,
          task,
          context,
          ...(factoryRun.accountId === undefined
            ? {}
            : {
                selection: {
                  accountId: factoryRun.accountId,
                  ...(factoryRun.model === undefined ? {} : { model: factoryRun.model }),
                },
              }),
        });
      }
      return output;
    });
    await Promise.all(requests.map((request) => this.executionDispatcher!.dispatch(request)));
    return requests.length;
  }

  async listProjects(): Promise<readonly Project[]> {
    return this.uow.transaction((tx) => tx.projects.list());
  }

  async hierarchy(projectId: ProjectId): Promise<ProjectHierarchy> {
    return this.uow.transaction(async (tx) => {
      const project = await this.requireProject(tx, projectId);
      const goals = await tx.goals.listByProject(projectId);
      const plans = await tx.plans.listByProject(projectId);
      const planRevisions = (
        await Promise.all(plans.map((plan) => tx.planRevisions.getCurrent(plan.id)))
      ).filter((revision): revision is PlanRevision => revision !== undefined);
      const tasks = await tx.tasks.listByProject(projectId);
      const planningSessions = await tx.planningSessions.listByProject(projectId);
      const planningDefaults = await tx.projectPlanningDefaults.getByProjectId(projectId);
      const vision = await tx.projectVisions.getCurrent(projectId);
      const factoryRuns = await tx.factoryRuns.listByProject(projectId);
      const agentRuns = await tx.agentRuns.listByProject(projectId);
      const attempts = await tx.attempts.listByAgentRunIds(agentRuns.map((run) => run.id));
      const workspaceIds = [...new Set(attempts.map((attempt) => attempt.workspaceId))];
      const workspaces = (
        await Promise.all(workspaceIds.map((workspaceId) => tx.workspaces.getById(workspaceId)))
      ).filter((workspace): workspace is Workspace => workspace !== undefined);
      const changeSets = await tx.changeSets.listByProject(projectId);
      const changeSetIds = changeSets.map((item) => item.id);
      const reviews = await tx.reviews.listByChangeSetIds(changeSetIds);
      const verificationEvidence = await tx.verificationEvidence.listByChangeSetIds(changeSetIds);
      const reviewFindings = await tx.reviewFindings.listByChangeSetIds(changeSetIds);
      const events = await tx.events.listByProject(projectId);
      return {
        project,
        ...(vision ? { vision } : {}),
        goals,
        plans,
        planRevisions,
        planningSessions,
        ...(planningDefaults ? { planningDefaults } : {}),
        tasks,
        factoryRuns,
        agentRuns,
        attempts,
        workspaces,
        changeSets,
        reviews,
        verificationEvidence,
        reviewFindings,
        events,
      };
    });
  }

  private async requireProject(tx: ApplicationTransaction, projectId: ProjectId): Promise<Project> {
    const project = await tx.projects.getById(projectId);
    if (!project) throw new Error(`Unknown Project ${projectId}`);
    return project;
  }

  private async record(
    tx: ApplicationTransaction,
    context: MutationContext,
    type: string,
    aggregateType: string,
    aggregateId: string,
    aggregateRevision: number,
    projectId: ProjectId,
    payload: unknown,
  ): Promise<void> {
    const occurredAt = this.clock.now().toISOString();
    await tx.events.append({
      id: this.ids.next<EventId>(),
      type,
      schemaVersion: 1,
      occurredAt,
      aggregateType,
      aggregateId,
      aggregateRevision,
      projectId,
      principalId: context.authority.principal.id,
      correlationId: context.correlationId as CorrelationId,
      payload,
    });
    await tx.audit.append({
      id: this.ids.next<AuditRecordId>(),
      occurredAt,
      principalId: context.authority.principal.id,
      action: type,
      targetType: aggregateType,
      targetId: aggregateId,
      projectId,
      disposition: "allowed",
      correlationId: context.correlationId,
      safeMetadata: { operationId: context.operationId },
    });
    await tx.outbox.append({
      id: this.ids.next<OutboxMessageId>(),
      topic: type,
      payload: { aggregateType, aggregateId, aggregateRevision, projectId, payload },
      occurredAt,
    });
  }
}
