import type {
  AuditRecordId,
  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,
  Review,
  ReviewFinding,
  Task,
  VerificationEvidence,
} 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";

export interface CreateProjectInput {
  readonly name: string;
  readonly repositoryUrl: 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 tasks: readonly Task[];
  readonly factoryRuns: readonly FactoryRun[];
  readonly agentRuns: readonly AgentRun[];
  readonly attempts: readonly Attempt[];
  readonly changeSets: readonly ChangeSet[];
  readonly reviews: readonly Review[];
  readonly verificationEvidence: readonly VerificationEvidence[];
  readonly reviewFindings: readonly ReviewFinding[];
}

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;
}

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

  async createProject(input: CreateProjectInput): Promise<Project> {
    const project: Project = {
      id: this.ids.next<ProjectId>(),
      name: requireText(input.name, "Project name"),
      repositoryUrl: validateRepositoryUrl(input.repositoryUrl),
      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,
      });
      return project;
    });
  }

  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 dispatchRequests: ExecutionDispatchRequest[] = [];
    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 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) {
        await tx.tasks.update({
          ...task,
          status: eligibleIds.has(task.id)
            ? "dispatched"
            : unsatisfied.length === 0
              ? "ready"
              : "blocked",
          revision: task.revision + 1,
        });
      }
      if (eligibleTasks.length > 0) {
        const factoryRun: FactoryRun = {
          id: this.ids.next<FactoryRunId>(),
          projectId,
          planRevisionId: revision.id,
          status: "queued",
          reason: "Automatic dependency-legal dispatch after Plan approval",
          revision: 1,
        };
        await tx.factoryRuns.insert(factoryRun);
        for (const task of eligibleTasks) {
          dispatchRequests.push({
            factoryRun,
            task,
            context,
            ...(selection === undefined ? {} : { selection }),
          });
        }
        await this.record(
          tx,
          context,
          "FactoryRunQueued",
          "FactoryRun",
          factoryRun.id,
          1,
          projectId,
          {
            planId,
            planRevisionId: revision.id,
            taskIds: eligibleTasks.map((task) => task.id),
          },
        );
      }
      const approved: Plan = { ...plan, status: "approved", revision: plan.revision + 1 };
      await tx.plans.update(approved);
      await this.record(
        tx,
        context,
        "PlanApproved",
        "Plan",
        plan.id,
        approved.revision,
        projectId,
        {
          planRevisionId: revision.id,
        },
      );
      return approved;
    });
    if (this.executionDispatcher && dispatchRequests.length > 0) {
      await Promise.all(
        dispatchRequests.map((request) => this.executionDispatcher!.dispatch(request)),
      );
    }
    return approved;
  }

  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 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 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);
      return {
        project,
        ...(vision ? { vision } : {}),
        goals,
        plans,
        planRevisions,
        tasks,
        factoryRuns,
        agentRuns,
        attempts,
        changeSets,
        reviews,
        verificationEvidence,
        reviewFindings,
      };
    });
  }

  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,
    });
  }
}
