import { randomUUID } from "node:crypto";
import { Hono, type Context } from "hono";
import {
  authorityContext,
  unsafeOpaqueId,
  type AttemptId,
  type ChangeSetId,
  type CorrelationId,
  type MutationContext,
  type OperationId,
  type PlanId,
  type ProviderOperationContext,
  type Principal,
  type PrincipalId,
  type ProjectId,
  type ReviewId,
  type ReviewFindingId,
  type TaskId,
} from "@awp/contracts";
import {
  MergeRefusedError,
  type AccountProvider,
  type ExecutionVerificationEvidenceInput,
  type I1LifecycleService,
  type WorkspaceExecutionDispatcher,
} from "@awp/application";
import { DependencyCycleError, InvalidDependencyError } from "@awp/domain";
import type { AccountLoginManager } from "./account-login.js";
import type { OperatorSessionManager } from "./operator-session.js";

export interface ControlPlaneHealth {
  readonly status: "ok" | "error";
  readonly database: "postgresql";
  readonly migrations: {
    readonly applied: number;
    readonly expected: number;
    readonly pending: number;
    readonly drift: readonly string[];
  };
}

export interface ControlPlaneHttpDependencies {
  readonly lifecycle: I1LifecycleService;
  readonly execution?: WorkspaceExecutionDispatcher;
  readonly accountProvider?: AccountProvider;
  readonly accountProviderContext?: (context: MutationContext) => ProviderOperationContext;
  readonly accountLogin?: AccountLoginManager;
  readonly sessionManager?: OperatorSessionManager;
  readonly health: () => Promise<ControlPlaneHealth>;
  readonly contextFactory?: (projectId?: ProjectId) => MutationContext;
  readonly systemContextFactory?: (projectId?: ProjectId) => MutationContext;
}

function mutationContext(principal: Principal, projectId?: ProjectId): MutationContext {
  const operationId = unsafeOpaqueId<OperationId>(`operation:${randomUUID()}`);
  const correlationId = unsafeOpaqueId<CorrelationId>(`correlation:${randomUUID()}`);
  return {
    operationId,
    correlationId,
    idempotencyKey: operationId,
    authority: authorityContext(principal, [], projectId),
  };
}

const systemPrincipal: Principal = {
  id: unsafeOpaqueId<PrincipalId>("principal:system:control-plane"),
  kind: "system",
  capabilities: [],
};

export function systemMutationContext(projectId?: ProjectId): MutationContext {
  return mutationContext(systemPrincipal, projectId);
}

class UnauthorizedError extends Error {}

function projectId(value: string): ProjectId {
  return unsafeOpaqueId<ProjectId>(value);
}
function changeSetId(value: string): ChangeSetId {
  return unsafeOpaqueId<ChangeSetId>(value);
}
function reviewId(value: string): ReviewId {
  return unsafeOpaqueId<ReviewId>(value);
}
function reviewFindingId(value: string): ReviewFindingId {
  return unsafeOpaqueId<ReviewFindingId>(value);
}
function taskId(value: string): TaskId {
  return unsafeOpaqueId<TaskId>(value);
}
function planId(value: string): PlanId {
  return unsafeOpaqueId<PlanId>(value);
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}
function requiredString(body: Record<string, unknown>, key: string): string {
  const value = body[key];
  if (typeof value !== "string" || !value.trim()) throw new Error(`${key} is required`);
  return value;
}
function toolCallArray(
  body: Record<string, unknown>,
): readonly { readonly name: string; readonly summary: string }[] {
  const value = body.toolCalls;
  if (!Array.isArray(value) || value.length === 0) {
    throw new Error("toolCalls must be a non-empty array");
  }
  return value.map((item) => {
    if (!isRecord(item)) throw new Error("toolCalls entries must be objects");
    return {
      name: requiredString(item, "name"),
      summary: requiredString(item, "summary"),
    };
  });
}
function verificationEvidenceArray(
  body: Record<string, unknown>,
): readonly ExecutionVerificationEvidenceInput[] {
  const value = body.evidence;
  if (!Array.isArray(value) || value.length === 0) {
    throw new Error("evidence must be a non-empty array");
  }
  return value.map((item) => {
    if (!isRecord(item)) throw new Error("evidence entries must be objects");
    const state = requiredString(item, "state");
    if (state !== "passed" && state !== "failed" && state !== "stale" && state !== "missing") {
      throw new Error("evidence state must be passed, failed, stale, or missing");
    }
    const required = item.required;
    if (required !== undefined && typeof required !== "boolean") {
      throw new Error("evidence required must be boolean when provided");
    }
    const details = item.details;
    if (details !== undefined && !isRecord(details)) {
      throw new Error("evidence details must be an object when provided");
    }
    return {
      name: requiredString(item, "name"),
      state,
      source: requiredString(item, "source"),
      observedAt: requiredString(item, "observedAt"),
      ...(required === undefined ? {} : { required }),
      ...(details === undefined ? {} : { details }),
    };
  });
}

function reviewFindingArray(body: Record<string, unknown>): readonly {
  readonly severity: "blocking" | "warning" | "recommendation" | "info";
  readonly summary: string;
}[] {
  const value = body.findings;
  if (value === undefined) return [];
  if (!Array.isArray(value)) throw new Error("findings must be an array");
  return value.map((item) => {
    if (!isRecord(item)) throw new Error("findings entries must be objects");
    const severity = requiredString(item, "severity");
    if (
      severity !== "blocking" &&
      severity !== "warning" &&
      severity !== "recommendation" &&
      severity !== "info"
    ) {
      throw new Error("finding severity must be blocking, warning, recommendation, or info");
    }
    return { severity, summary: requiredString(item, "summary") };
  });
}

function stringArray(body: Record<string, unknown>, key: string): readonly string[] {
  const value = body[key];
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
    throw new Error(`${key} must be an array of strings`);
  }
  return value;
}

function optionalStringArray(
  body: Record<string, unknown>,
  key: string,
): readonly string[] | undefined {
  if (body[key] === undefined) return undefined;
  return stringArray(body, key);
}
function candidateChanges(
  body: Record<string, unknown>,
): readonly { readonly path: string; readonly kind: "add" | "modify" | "delete" }[] {
  const value = body.changes;
  if (!Array.isArray(value)) throw new Error("changes must be an array");
  return value.map((item) => {
    if (!isRecord(item)) throw new Error("changes entries must be objects");
    const path = requiredString(item, "path");
    const kind = requiredString(item, "kind");
    if (kind !== "add" && kind !== "modify" && kind !== "delete") {
      throw new Error("changes kind must be add, modify, or delete");
    }
    return { path, kind };
  });
}
async function bodyRecord(request: {
  json<T = unknown>(): Promise<T>;
}): Promise<Record<string, unknown>> {
  const body = await request.json<unknown>();
  if (!isRecord(body)) throw new Error("JSON body must be an object");
  return body;
}

export function createControlPlaneApp(deps: ControlPlaneHttpDependencies): Hono {
  const app = new Hono();
  const systemContext = deps.systemContextFactory ?? systemMutationContext;
  const sessionToken = (c: Context): string => c.req.header("x-awp-session")?.trim() ?? "";
  const context = async (c: Context, projectId?: ProjectId): Promise<MutationContext> => {
    if (deps.sessionManager) {
      const principal = await deps.sessionManager.resolve(sessionToken(c));
      if (!principal) throw new UnauthorizedError("Authenticated operator session is required");
      return mutationContext(principal, projectId);
    }
    if (deps.contextFactory) return deps.contextFactory(projectId);
    throw new UnauthorizedError("Authentication is not configured");
  };

  app.get("/health", async (c) => {
    const health = await deps.health();
    return c.json(health, health.status === "ok" ? 200 : 503);
  });

  app.post("/internal/auth/sessions", async (c) => {
    if (!deps.sessionManager) return c.json({ error: "authentication-not-configured" }, 503);
    const body = await bodyRecord(c.req);
    const created = await deps.sessionManager.login(
      requiredString(body, "password"),
      typeof body.userAgent === "string" ? body.userAgent : undefined,
    );
    if (!created) return c.json({ error: "invalid-credentials" }, 401);
    return c.json({ token: created.token, expiresAt: created.session.expiresAt }, 201);
  });

  app.get("/internal/auth/session", async (c) => {
    const resolved = await context(c);
    return c.json({ principal: { id: resolved.authority.principal.id, kind: "human" } });
  });

  app.post("/internal/auth/session/revoke", async (c) => {
    if (!deps.sessionManager) return c.json({ error: "authentication-not-configured" }, 503);
    const token = sessionToken(c);
    if (!(await deps.sessionManager.resolve(token))) {
      return c.json({ error: "unauthorized" }, 401);
    }
    await deps.sessionManager.revoke(token);
    return c.json({ revoked: true });
  });

  app.get("/internal/providers/accounts", async (c) => {
    const requestContext = await context(c);
    if (!deps.accountProvider || !deps.accountProviderContext)
      return c.json({ error: "account-provider-disabled" }, 404);
    const [descriptor, result] = await Promise.all([
      deps.accountProvider.describe(),
      deps.accountProvider.listAccounts(deps.accountProviderContext(requestContext)),
    ]);
    return c.json({
      provider: descriptor,
      accounts: result.value,
      references: result.references,
      observedAt: result.observedAt,
    });
  });

  app.post("/internal/providers/accounts/login", async (c) => {
    await context(c);
    if (!deps.accountLogin) return c.json({ error: "account-login-disabled" }, 404);
    const body = await bodyRecord(c.req);
    const provider = requiredString(body, "provider");
    if (provider !== "codex") {
      return c.json(
        { error: "unsupported-account-login", message: "Slice 1 supports Codex OAuth login" },
        400,
      );
    }
    return c.json(await deps.accountLogin.startCodex(), 202);
  });

  app.get("/internal/providers/accounts/login/:sessionId", async (c) => {
    await context(c);
    if (!deps.accountLogin) return c.json({ error: "account-login-disabled" }, 404);
    const status = deps.accountLogin.get(c.req.param("sessionId"));
    if (!status) return c.json({ error: "account-login-not-found" }, 404);
    return c.json(status);
  });

  app.get("/internal/projects", async (c) => {
    await context(c);
    return c.json({ projects: await deps.lifecycle.listProjects() });
  });

  app.post("/internal/projects", async (c) => {
    const body = await bodyRecord(c.req);
    const requiredChecks = optionalStringArray(body, "requiredChecks");
    const project = await deps.lifecycle.createProject({
      name: requiredString(body, "name"),
      repositoryUrl: requiredString(body, "repositoryUrl"),
      ...(requiredChecks === undefined ? {} : { requiredChecks }),
      context: await context(c),
    });
    return c.json({ project }, 201);
  });

  app.put("/internal/projects/:projectId/verification-policy", async (c) => {
    const id = projectId(c.req.param("projectId"));
    const body = await bodyRecord(c.req);
    const project = await deps.lifecycle.setProjectRequiredChecks(
      id,
      stringArray(body, "requiredChecks"),
      await context(c, id),
    );
    return c.json({ project });
  });

  app.get("/internal/projects/:projectId", async (c) => {
    const id = projectId(c.req.param("projectId"));
    await context(c, id);
    return c.json(await deps.lifecycle.hierarchy(id));
  });

  app.post("/internal/projects/:projectId/vision", async (c) => {
    const id = projectId(c.req.param("projectId"));
    const body = await bodyRecord(c.req);
    const vision = await deps.lifecycle.setProjectVision(
      id,
      requiredString(body, "summary"),
      await context(c, id),
    );
    return c.json({ vision }, 201);
  });

  app.post("/internal/projects/:projectId/goals", async (c) => {
    const id = projectId(c.req.param("projectId"));
    const body = await bodyRecord(c.req);
    const goal = await deps.lifecycle.createGoal({
      projectId: id,
      title: requiredString(body, "title"),
      successCriteria: stringArray(body, "successCriteria"),
      context: await context(c, id),
    });
    return c.json({ goal }, 201);
  });

  app.post("/internal/projects/:projectId/plans", async (c) => {
    const id = projectId(c.req.param("projectId"));
    const body = await bodyRecord(c.req);
    const created = await deps.lifecycle.createPlan({
      projectId: id,
      title: requiredString(body, "title"),
      taskTitles: stringArray(body, "taskTitles"),
      context: await context(c, id),
    });
    return c.json(created, 201);
  });

  app.post("/internal/projects/:projectId/tasks/:taskId/dependencies", async (c) => {
    const id = projectId(c.req.param("projectId"));
    const body = await bodyRecord(c.req);
    const task = await deps.lifecycle.addDependency(
      id,
      taskId(c.req.param("taskId")),
      taskId(requiredString(body, "prerequisiteTaskId")),
      await context(c, id),
    );
    return c.json({ task });
  });

  app.post("/internal/projects/:projectId/plans/:planId/approve", async (c) => {
    const id = projectId(c.req.param("projectId"));
    let accountId: string | undefined;
    let model: string | undefined;
    if (deps.accountProvider && deps.accountProviderContext) {
      const body = await bodyRecord(c.req);
      accountId = requiredString(body, "accountId");
      model = typeof body.model === "string" && body.model.trim() ? body.model.trim() : undefined;
      const accounts = await deps.accountProvider.listAccounts(
        deps.accountProviderContext(await context(c, id)),
      );
      if (!accounts.value.some((account) => account.accountKey === accountId)) {
        return c.json(
          {
            error: "account-not-found",
            message: "Selected account is not available in K3s Subrouter",
          },
          409,
        );
      }
    }
    const plan = await deps.lifecycle.approvePlan(
      id,
      planId(c.req.param("planId")),
      await context(c, id),
      accountId === undefined
        ? undefined
        : { accountId, ...(model === undefined ? {} : { model }) },
    );
    return c.json({ plan });
  });

  app.post("/internal/reviews/:reviewId/approve", async (c) => {
    const requestContext = await context(c);
    if (!deps.execution) return c.json({ error: "execution-disabled" }, 404);
    const review = await deps.execution.approveReview(
      reviewId(c.req.param("reviewId")),
      requestContext,
    );
    return c.json({ review });
  });

  app.post("/internal/changesets/:changeSetId/required-checks/refresh", async (c) => {
    const requestContext = await context(c);
    if (!deps.execution) return c.json({ error: "execution-disabled" }, 404);
    const evidence = await deps.execution.refreshRequiredChecks(
      changeSetId(c.req.param("changeSetId")),
      requestContext,
    );
    return c.json({ evidence });
  });

  app.post("/internal/changesets/:changeSetId/evidence", async (c) => {
    const requestContext = await context(c);
    if (!deps.execution) return c.json({ error: "execution-disabled" }, 404);
    const body = await bodyRecord(c.req);
    const [evidenceInput] = verificationEvidenceArray({ evidence: [body] });
    const evidence = await deps.execution.recordVerificationEvidence(
      { changeSetId: changeSetId(c.req.param("changeSetId")), ...evidenceInput! },
      requestContext,
    );
    return c.json({ evidence }, 201);
  });

  app.post("/internal/changesets/:changeSetId/findings", async (c) => {
    const requestContext = await context(c);
    if (!deps.execution) return c.json({ error: "execution-disabled" }, 404);
    const body = await bodyRecord(c.req);
    const severity = requiredString(body, "severity");
    if (
      severity !== "blocking" &&
      severity !== "warning" &&
      severity !== "recommendation" &&
      severity !== "info"
    ) {
      throw new Error("severity must be blocking, warning, recommendation, or info");
    }
    const finding = await deps.execution.recordReviewFinding(
      {
        changeSetId: changeSetId(c.req.param("changeSetId")),
        severity,
        summary: requiredString(body, "summary"),
        ...(typeof body.source === "string" && body.source.trim()
          ? { source: body.source.trim() }
          : {}),
      },
      requestContext,
    );
    return c.json({ finding }, 201);
  });

  app.post("/internal/findings/:findingId/resolve", async (c) => {
    const requestContext = await context(c);
    if (!deps.execution) return c.json({ error: "execution-disabled" }, 404);
    const finding = await deps.execution.resolveReviewFinding(
      reviewFindingId(c.req.param("findingId")),
      requestContext,
    );
    return c.json({ finding });
  });

  app.post("/internal/changesets/:changeSetId/merge", async (c) => {
    const requestContext = await context(c);
    if (!deps.execution) return c.json({ error: "execution-disabled" }, 404);
    const changeSet = await deps.execution.requestMerge(
      changeSetId(c.req.param("changeSetId")),
      requestContext,
    );
    return c.json({ changeSet });
  });

  app.post("/internal/execution/fail", async (c) => {
    if (!deps.execution) return c.json({ error: "execution-disabled" }, 404);
    const body = await bodyRecord(c.req);
    const attempt = await deps.execution.failAttempt(
      {
        attemptId: unsafeOpaqueId<AttemptId>(requiredString(body, "attemptId")),
        token: requiredString(body, "token"),
        reason: requiredString(body, "reason"),
        workspaceCheckpointDigest: requiredString(body, "workspaceCheckpointDigest"),
        workspaceCheckpointedAt: requiredString(body, "workspaceCheckpointedAt"),
      },
      systemContext(),
    );
    return c.json({ attempt }, 201);
  });

  app.post("/internal/execution/review", async (c) => {
    if (!deps.execution) return c.json({ error: "execution-disabled" }, 404);
    const body = await bodyRecord(c.req);
    const disposition = requiredString(body, "disposition");
    if (
      disposition !== "approved" &&
      disposition !== "changes-requested" &&
      disposition !== "blocked"
    ) {
      throw new Error("disposition must be approved, changes-requested, or blocked");
    }
    const review = await deps.execution.completeReview(
      {
        attemptId: unsafeOpaqueId<AttemptId>(requiredString(body, "attemptId")),
        reviewId: reviewId(requiredString(body, "reviewId")),
        token: requiredString(body, "token"),
        candidateDigest: requiredString(body, "candidateDigest"),
        workspaceCheckpointDigest: requiredString(body, "workspaceCheckpointDigest"),
        workspaceCheckpointedAt: requiredString(body, "workspaceCheckpointedAt"),
        disposition,
        toolCalls: toolCallArray(body),
        findings: reviewFindingArray(body),
      },
      systemContext(),
    );
    return c.json({ review }, 201);
  });

  app.post("/internal/execution/complete", async (c) => {
    if (!deps.execution) return c.json({ error: "execution-disabled" }, 404);
    const body = await bodyRecord(c.req);
    const attemptId = unsafeOpaqueId<AttemptId>(requiredString(body, "attemptId"));
    const changeSet = await deps.execution.complete(
      {
        attemptId,
        token: requiredString(body, "token"),
        diff: Buffer.from(requiredString(body, "diffBase64"), "base64").toString("utf8"),
        baseRevision: requiredString(body, "baseRevision"),
        candidateTreeDigest: requiredString(body, "candidateTreeDigest"),
        workspaceCheckpointDigest: requiredString(body, "workspaceCheckpointDigest"),
        workspaceCheckpointedAt: requiredString(body, "workspaceCheckpointedAt"),
        changedPaths: stringArray(body, "changedPaths"),
        changes: candidateChanges(body),
        toolCalls: toolCallArray(body),
        evidence: verificationEvidenceArray(body),
      },
      systemContext(),
    );
    return c.json({ changeSet }, 201);
  });

  app.notFound((c) => c.json({ error: "not-found" }, 404));
  app.onError((error, c) => {
    if (error instanceof UnauthorizedError) {
      return c.json({ error: "unauthorized", message: error.message }, 401);
    }
    if (error instanceof DependencyCycleError) {
      return c.json(
        {
          error: "dependency-cycle",
          message: error.message,
          cycle: error.cycle,
        },
        409,
      );
    }
    if (error instanceof MergeRefusedError) {
      return c.json({ error: "merge-refused", message: error.message }, 409);
    }
    if (error instanceof InvalidDependencyError) {
      return c.json({ error: "invalid-dependency", message: error.message }, 400);
    }
    const message = error instanceof Error ? error.message : "Unexpected control-plane error";
    if (/^Unknown /.test(message)) return c.json({ error: "not-found", message }, 404);
    return c.json({ error: "invalid-request", message }, 400);
  });

  return app;
}
