import type {
  AgentRunId,
  AttemptId,
  FactoryRunId,
  GoalId,
  I1AttentionReason,
  I1ChangeSetReviewReadModel,
  I1FactoryRunDetailReadModel,
  I1FailureDetail,
  I1MergedResultIdentity,
  I1NextAction,
  I1RealtimeEntityType,
  I1RealtimeProjectionEnvelope,
  I1RepositoryRevisionIdentity,
  I1RetryDisposition,
  I1ReviewFinding,
  I1VerificationEvidence,
  I1WaitReason,
  I1WipSafety,
  PlanId,
  PrincipalId,
  ProjectId,
  TaskId,
} from "@awp/contracts";
import {
  buildQueue,
  type AgentRun,
  type Attempt,
  type ChangeSet,
  type FactoryRun,
  type Goal,
  type Plan,
  type Project,
  type QueueEntry,
  type Review,
  type Task,
} from "@awp/domain";

export interface I1GoalReadModel {
  readonly id: GoalId;
  readonly title: string;
  readonly status: Goal["status"];
}
export interface I1PlanReadModel {
  readonly id: PlanId;
  readonly title: string;
  readonly status: Plan["status"];
  readonly goalIds: readonly GoalId[];
}
export interface I1FactoryRunReadModel {
  readonly id: FactoryRunId;
  readonly status: FactoryRun["status"];
  readonly reason?: string;
}
export interface I1ProjectOverviewReadModel {
  readonly project: {
    readonly id: ProjectId;
    readonly name: string;
    readonly repositoryUrl: string;
    readonly status: Project["status"];
  };
  readonly goals: readonly I1GoalReadModel[];
  readonly plans: readonly I1PlanReadModel[];
  readonly queue: readonly QueueEntry[];
  readonly factoryRuns: readonly I1FactoryRunReadModel[];
}
export function projectOverviewReadModel(input: {
  project: Project;
  goals: readonly Goal[];
  plans: readonly Plan[];
  planGoalIds: ReadonlyMap<PlanId, readonly GoalId[]>;
  tasks: readonly Task[];
  queueOrder: readonly TaskId[];
  factoryRuns: readonly FactoryRun[];
}): I1ProjectOverviewReadModel {
  return {
    project: {
      id: input.project.id,
      name: input.project.name,
      repositoryUrl: input.project.repositoryUrl,
      status: input.project.status,
    },
    goals: input.goals.map((g) => ({ id: g.id, title: g.title, status: g.status })),
    plans: input.plans.map((p) => ({
      id: p.id,
      title: p.title,
      status: p.status,
      goalIds: input.planGoalIds.get(p.id) ?? [],
    })),
    queue: buildQueue(input.tasks, input.queueOrder),
    factoryRuns: input.factoryRuns.map((r) =>
      r.reason === undefined
        ? { id: r.id, status: r.status }
        : { id: r.id, status: r.status, reason: r.reason },
    ),
  };
}

export interface I1WorkProjectionFacts {
  readonly taskId: TaskId;
  readonly currentActorId?: PrincipalId;
  readonly stage: string;
  readonly lastProgressAt?: string;
  readonly deadlineAt?: string;
  readonly wait?: I1WaitReason;
  readonly attention: I1AttentionReason;
  readonly nextAction: I1NextAction;
}

export interface I1FactoryRunDetailInput {
  readonly factoryRun: FactoryRun;
  readonly tasks: readonly Task[];
  readonly agentRuns: readonly AgentRun[];
  readonly attempts: readonly Attempt[];
  readonly currentAttemptIds: ReadonlyMap<AgentRunId, AttemptId>;
  readonly workFacts: readonly I1WorkProjectionFacts[];
  readonly stage: string;
  readonly lastProgressAt?: string;
  readonly deadlineAt?: string;
  readonly wait?: I1WaitReason;
  readonly failure?: I1FailureDetail;
  readonly retryDisposition: I1RetryDisposition;
  readonly wip: I1WipSafety;
  readonly liveExecutorPresent: boolean;
  readonly historicalAvailable: boolean;
  readonly continuation: I1FactoryRunDetailReadModel["continuation"];
}

function requireNonEmpty(value: string, field: string): void {
  if (value.trim().length === 0) throw new Error(`${field} must not be empty`);
}

export function factoryRunDetailReadModel(
  input: I1FactoryRunDetailInput,
): I1FactoryRunDetailReadModel {
  requireNonEmpty(input.stage, "FactoryRun stage");
  if (input.continuation.factoryRunId !== input.factoryRun.id) {
    throw new Error("Continuation identity must reference the FactoryRun");
  }
  requireNonEmpty(input.continuation.continuationKey, "Continuation key");

  const taskById = new Map(input.tasks.map((task) => [task.id, task] as const));
  const factsByTask = new Map(input.workFacts.map((facts) => [facts.taskId, facts] as const));
  if (factsByTask.size !== input.workFacts.length)
    throw new Error("Work projection facts must be unique per Task");
  for (const task of input.tasks) {
    if (
      task.projectId !== input.factoryRun.projectId ||
      task.planRevisionId !== input.factoryRun.planRevisionId
    ) {
      throw new Error(`Task ${task.id} is outside the FactoryRun Project/PlanRevision`);
    }
    if (!factsByTask.has(task.id))
      throw new Error(`Missing work projection facts for Task ${task.id}`);
  }
  for (const facts of input.workFacts) {
    if (!taskById.has(facts.taskId))
      throw new Error(`Work projection references unknown Task ${facts.taskId}`);
    requireNonEmpty(facts.stage, `Task ${facts.taskId} stage`);
  }

  const agentRunById = new Map(input.agentRuns.map((run) => [run.id, run] as const));
  if (agentRunById.size !== input.agentRuns.length) throw new Error("AgentRuns must be unique");
  for (const run of input.agentRuns) {
    if (run.factoryRunId !== input.factoryRun.id)
      throw new Error(`AgentRun ${run.id} belongs to another FactoryRun`);
    if (!taskById.has(run.taskId))
      throw new Error(`AgentRun ${run.id} references unknown Task ${run.taskId}`);
  }

  const attemptsByAgent = new Map<AgentRunId, Attempt[]>();
  for (const attempt of input.attempts) {
    if (!agentRunById.has(attempt.agentRunId)) {
      throw new Error(`Attempt ${attempt.id} references unknown AgentRun ${attempt.agentRunId}`);
    }
    const attempts = attemptsByAgent.get(attempt.agentRunId) ?? [];
    attempts.push(attempt);
    attemptsByAgent.set(attempt.agentRunId, attempts);
  }

  const currentAgentRunByTask = new Map<TaskId, AgentRunId>();
  const agentRuns = input.agentRuns.map((run) => {
    currentAgentRunByTask.set(run.taskId, run.id);
    const attempts = attemptsByAgent.get(run.id) ?? [];
    const currentAttemptId = input.currentAttemptIds.get(run.id);
    if (
      currentAttemptId !== undefined &&
      !attempts.some((attempt) => attempt.id === currentAttemptId)
    ) {
      throw new Error(`Current Attempt ${currentAttemptId} does not belong to AgentRun ${run.id}`);
    }
    return {
      id: run.id,
      taskId: run.taskId,
      status: run.status,
      ...(run.reason === undefined ? {} : { reason: run.reason }),
      ...(currentAttemptId === undefined ? {} : { currentAttemptId }),
      attempts: attempts.map((attempt) => ({
        id: attempt.id,
        workspaceId: attempt.workspaceId,
        status: attempt.status,
        ...(attempt.providerId === undefined ? {} : { providerId: attempt.providerId }),
        ...(attempt.accountId === undefined ? {} : { accountId: attempt.accountId }),
        ...(attempt.model === undefined ? {} : { model: attempt.model }),
        selection: {
          kind: attempt.selection.kind,
          reason: attempt.selection.reason,
          ...(attempt.selection.previousAttemptId === undefined
            ? {}
            : { previousAttemptId: attempt.selection.previousAttemptId }),
        },
        ...(attempt.providerReference === undefined
          ? {}
          : { providerReference: attempt.providerReference }),
      })),
    };
  });

  const work = input.tasks.map((task) => {
    const facts = factsByTask.get(task.id)!;
    const currentAgentRunId = currentAgentRunByTask.get(task.id);
    return {
      taskId: task.id,
      planRevisionId: task.planRevisionId,
      title: task.title,
      status: task.status,
      ...(facts.currentActorId === undefined ? {} : { currentActorId: facts.currentActorId }),
      ...(currentAgentRunId === undefined ? {} : { currentAgentRunId }),
      stage: facts.stage,
      ...(facts.lastProgressAt === undefined ? {} : { lastProgressAt: facts.lastProgressAt }),
      ...(facts.deadlineAt === undefined ? {} : { deadlineAt: facts.deadlineAt }),
      ...(facts.wait === undefined ? {} : { wait: facts.wait }),
      attention: facts.attention,
      nextAction: facts.nextAction,
    };
  });

  return {
    id: input.factoryRun.id,
    revision: input.factoryRun.revision,
    projectId: input.factoryRun.projectId,
    planRevisionId: input.factoryRun.planRevisionId,
    status: input.factoryRun.status,
    ...(input.factoryRun.reason === undefined ? {} : { reason: input.factoryRun.reason }),
    stage: input.stage,
    ...(input.lastProgressAt === undefined ? {} : { lastProgressAt: input.lastProgressAt }),
    ...(input.deadlineAt === undefined ? {} : { deadlineAt: input.deadlineAt }),
    work,
    agentRuns,
    ...(input.wait === undefined ? {} : { wait: input.wait }),
    ...(input.failure === undefined ? {} : { failure: input.failure }),
    retryDisposition: input.retryDisposition,
    wip: input.wip,
    liveExecutorPresent: input.liveExecutorPresent,
    historicalAvailable: input.historicalAvailable,
    continuation: input.continuation,
  };
}

export interface I1ChangeSetReviewInput {
  readonly changeSet: ChangeSet;
  readonly reviews: readonly Review[];
  readonly findings: readonly I1ReviewFinding[];
  readonly evidence: readonly I1VerificationEvidence[];
  readonly expectedTarget: I1RepositoryRevisionIdentity;
  readonly publicationFidelitySatisfied: boolean;
  readonly policySatisfied: boolean;
  readonly authoritySatisfied: boolean;
  readonly targetHeadCurrent: boolean;
  readonly mergedResult?: I1MergedResultIdentity;
}

export function changeSetReviewReadModel(
  input: I1ChangeSetReviewInput,
): I1ChangeSetReviewReadModel {
  requireNonEmpty(input.expectedTarget.reference, "Expected target reference");
  requireNonEmpty(input.expectedTarget.revision, "Expected target revision");
  for (const review of input.reviews) {
    if (review.changeSetId !== input.changeSet.id)
      throw new Error(`Review ${review.id} belongs to another ChangeSet`);
    if (review.candidateDigest !== input.changeSet.candidateDigest) {
      throw new Error(`Review ${review.id} is bound to another candidate`);
    }
  }
  for (const evidence of input.evidence) {
    if (evidence.candidateDigest !== input.changeSet.candidateDigest) {
      throw new Error(`Evidence ${evidence.id} is bound to another candidate`);
    }
  }
  if (input.changeSet.status === "merged" && input.mergedResult === undefined) {
    throw new Error("Merged ChangeSet requires resulting repository identity");
  }
  if (input.changeSet.status !== "merged" && input.mergedResult !== undefined) {
    throw new Error("Only a merged ChangeSet may expose merged result identity");
  }

  const blockingFindingsClear = !input.findings.some(
    (finding) => finding.severity === "blocking" && !finding.resolved,
  );
  const reviewSatisfied =
    blockingFindingsClear &&
    input.reviews.some(
      (review) => review.status === "submitted" && review.disposition === "approved",
    );
  const evidenceSatisfied =
    input.evidence.length > 0 && input.evidence.every((evidence) => evidence.state === "passed");
  const candidateStateSatisfied = input.changeSet.status === "ready-to-merge";
  const terminal = input.changeSet.status === "merged";

  const conditions = [
    {
      code: "candidate-state",
      satisfied: candidateStateSatisfied,
      summary: "Exact ChangeSet candidate is ready to merge",
    },
    {
      code: "review",
      satisfied: reviewSatisfied,
      summary: "Independent Review is approved with no unresolved blocking findings",
    },
    {
      code: "evidence",
      satisfied: evidenceSatisfied,
      summary: "Required evidence is current and passed for this candidate",
    },
    {
      code: "publication",
      satisfied: input.publicationFidelitySatisfied,
      summary: "Published candidate matches the immutable ChangeSet",
    },
    {
      code: "policy",
      satisfied: input.policySatisfied,
      summary: "Merge policy and approvals are satisfied",
    },
    {
      code: "authority",
      satisfied: input.authoritySatisfied,
      summary: "Trusted control-plane merge authority is available",
    },
    {
      code: "target-head",
      satisfied: input.targetHeadCurrent,
      summary: "Expected target head is still current",
    },
  ] as const;

  const failedCondition = conditions.find((condition) => !condition.satisfied);
  const action = terminal
    ? { available: false, reasonCode: "terminal" as const, summary: "ChangeSet is already merged" }
    : failedCondition === undefined
      ? {
          available: true,
          reasonCode: "ready" as const,
          summary: "Protected Merge may be requested; execution must revalidate all conditions",
        }
      : {
          available: false,
          reasonCode: failedCondition.code,
          summary: failedCondition.summary,
        };

  return {
    id: input.changeSet.id,
    revision: input.changeSet.revision,
    projectId: input.changeSet.projectId,
    taskId: input.changeSet.taskId,
    status: input.changeSet.status,
    baseIdentity: input.changeSet.baseIdentity,
    candidateDigest: input.changeSet.candidateDigest,
    candidate: {
      treeDigest: input.changeSet.candidateManifest.treeDigest,
      patchDigest: input.changeSet.candidateManifest.patchDigest,
      changedPaths: [...input.changeSet.candidateManifest.changedPaths],
      changes: input.changeSet.candidateManifest.changes.map((change) => ({ ...change })),
    },
    reviews: input.reviews.map((review) => ({
      id: review.id,
      reviewerPrincipalId: review.reviewerPrincipalId,
      status: review.status,
      ...(review.disposition === undefined ? {} : { disposition: review.disposition }),
    })),
    findings: input.findings,
    evidence: input.evidence,
    expectedTarget: input.expectedTarget,
    mergeGate: { conditions, action },
    ...(input.mergedResult === undefined ? {} : { mergedResult: input.mergedResult }),
  };
}

export function realtimeProjectionEnvelope<T>(input: {
  readonly entityType: I1RealtimeEntityType;
  readonly entityId: string;
  readonly revision: number;
  readonly sequence: number;
  readonly observedAt: string;
  readonly payload: T;
}): I1RealtimeProjectionEnvelope<T> {
  requireNonEmpty(input.entityId, "Realtime entity identity");
  requireNonEmpty(input.observedAt, "Realtime observedAt");
  if (!Number.isInteger(input.revision) || input.revision < 0)
    throw new Error("Realtime revision must be a non-negative integer");
  if (!Number.isInteger(input.sequence) || input.sequence < 0)
    throw new Error("Realtime sequence must be a non-negative integer");
  return { ...input };
}
