import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { serve, type ServerType } from "@hono/node-server";
import {
  DurableExecutionDispatcher,
  I1_DURABLE_DISPATCH_WORKFLOW_KIND,
  I1LifecycleService,
  ProviderExecutionWorker,
  WorkspaceExecutionDispatcher,
  type AttemptRepository,
  type Clock,
  type ExecutionDispatcher,
  type IdGenerator,
} from "@awp/application";
import {
  unsafeOpaqueId,
  type AwpId,
  type ConnectionId,
  type CredentialReferenceId,
  type ProviderId,
} from "@awp/contracts";
import { createPostgresRuntime, type PostgresRuntime } from "@awp/persistence";
import { SubrouterAccountProvider, SubrouterHttpClient } from "@awp/provider-account-subrouter";
import { AcpAgentProvider } from "@awp/provider-agent-acp";
import { FabroFactoryProvider, FabroHttpClient } from "@awp/provider-factory-fabro";
import { startNativeDbosProvider } from "@awp/provider-workflow-dbos";
import {
  KubernetesApiTransport,
  KubernetesWorkspaceProvider,
  WorkspaceProfileRegistry,
  type WorkspaceExecutionProfile,
} from "@awp/provider-workspace-kubernetes";
import { createControlPlaneApp, ownerMutationContext } from "./app.js";
import { AccountLoginManager } from "./account-login.js";
import { KubernetesAcpNativeClient } from "./kubernetes-acp-client.js";
import { LocalGitTrustedMergeAdapter } from "./local-git-merge.js";

export interface RunningControlPlane {
  readonly port: number;
  readonly runtime: PostgresRuntime;
  readonly server: ServerType;
  close(): Promise<void>;
}

const clock: Clock = { now: () => new Date() };
const ids: IdGenerator = {
  next<T extends AwpId>(): T {
    return unsafeOpaqueId<T>(randomUUID());
  },
};

function attemptRepository(runtime: PostgresRuntime): AttemptRepository {
  return {
    getById: (id) => runtime.uow.transaction((tx) => tx.attempts.getById(id)),
    insert: (attempt) => runtime.uow.transaction((tx) => tx.attempts.insert(attempt)),
    update: (attempt) => runtime.uow.transaction((tx) => tx.attempts.update(attempt)),
    listByAgentRunIds: (ids) => runtime.uow.transaction((tx) => tx.attempts.listByAgentRunIds(ids)),
    bindProviderReference: (attemptId, reference) =>
      runtime.uow.transaction((tx) => tx.attempts.bindProviderReference(attemptId, reference)),
  };
}

function workspaceProfile(
  env: Readonly<Record<string, string | undefined>>,
): WorkspaceExecutionProfile {
  const namespace = env.AWP_WORKSPACE_NAMESPACE ?? "awp-workspaces";
  if (env.AWP_NATIVE_ACP_ENABLED === "1") {
    const image = env.AWP_WORKSPACE_IMAGE?.trim();
    if (!image) throw new Error("AWP_WORKSPACE_IMAGE is required for native ACP execution");
    if (!/@sha256:[0-9a-f]{64}$/iu.test(image)) {
      throw new Error("AWP_WORKSPACE_IMAGE must be an immutable OCI digest reference");
    }
    return {
      key: "golive-native",
      namespace,
      image,
      persistent: true,
      storageSize: env.AWP_WORKSPACE_STORAGE ?? "2Gi",
      requests: { cpu: "100m", memory: "256Mi" },
      limits: { cpu: "2", memory: "2Gi" },
      allowedEgressCidrs: ["100.64.0.0/10"],
      allowedEgressPeers: [
        {
          namespaceLabels: { "kubernetes.io/metadata.name": "awp-system" },
          podLabels: { "app.kubernetes.io/name": "awp-model-gateway" },
          ports: [32180],
        },
      ],
      imagePullSecrets: ["awp-ghcr-pull"],
      requireImageDigest: true,
      allowDns: true,
      secretProjections: [],
      provenance: {
        image: "dogfood:AWP_WORKSPACE_IMAGE",
        environment: "native-acp-runner",
      },
    };
  }

  const initialDelaySeconds = Number(env.AWP_AGENT_START_DELAY_SECONDS ?? "8");
  if (
    !Number.isInteger(initialDelaySeconds) ||
    initialDelaySeconds < 1 ||
    initialDelaySeconds > 300
  ) {
    throw new Error("AWP_AGENT_START_DELAY_SECONDS must be an integer from 1 through 300");
  }
  const patchCommands =
    env.AWP_AGENT_PATCH_FIXTURE_MODE === "modify-readme"
      ? [
          "path=README.md",
          "patch=/tmp/awp.patch",
          `printf 'diff --git a/README.md b/README.md\\n--- a/README.md\\n+++ b/README.md\\n@@ -1 +1 @@\\n-AWP live fixture\\n+AWP AgentRun %s\\n' "$AWP_AGENT_RUN_ID" > "$patch"`,
        ]
      : [
          "path=golive-artifacts/$AWP_AGENT_RUN_ID.txt",
          "patch=/tmp/awp.patch",
          `printf 'diff --git a/%s b/%s\\nnew file mode 100644\\n--- /dev/null\\n+++ b/%s\\n@@ -0,0 +1 @@\\n+AWP AgentRun %s\\n' "$path" "$path" "$path" "$AWP_AGENT_RUN_ID" > "$patch"`,
        ];

  return {
    key: "golive-native",
    namespace,
    image: env.AWP_WORKSPACE_IMAGE ?? "busybox:1.36.1",
    command: [
      "sh",
      "-lc",
      [
        `sleep ${initialDelaySeconds}`,
        'if [ "$AWP_FORCE_FIRST_ATTEMPT_FAILURE" = "1" ] && [ "$AWP_SELECTION_KIND" = "initial" ]; then printf \'preserved WIP from %s\\n\' "$AWP_ATTEMPT_ID" > /workspace/preserved-wip.txt; printf \'{"attemptId":"%s","token":"%s","reason":"injected non-zero agent exit 17"}\' "$AWP_ATTEMPT_ID" "$AWP_CALLBACK_TOKEN" > /tmp/awp-failure.json; until wget -qO- --header=\'Content-Type: application/json\' --post-file=/tmp/awp-failure.json "$AWP_FAILURE_URL"; do sleep 1; done; exit 17; fi',
        ...patchCommands,
        "encoded=$(base64 < \"$patch\" | tr -d '\\n')",
        'printf \'{"attemptId":"%s","token":"%s","diffBase64":"%s","baseRevision":"0000000000000000000000000000000000000000","candidateTreeDigest":"1111111111111111111111111111111111111111","changedPaths":["%s"],"changes":[{"path":"%s","kind":"add"}],"toolCalls":[{"name":"write_patch","summary":"Created the candidate unified diff"}]}\' "$AWP_ATTEMPT_ID" "$AWP_CALLBACK_TOKEN" "$encoded" "$path" "$path" > /tmp/awp-result.json',
        "until wget -qO- --header='Content-Type: application/json' --post-file=/tmp/awp-result.json \"$AWP_CALLBACK_URL\"; do sleep 1; done",
        "sleep 2",
      ].join("; "),
    ],
    persistent: true,
    storageSize: env.AWP_WORKSPACE_STORAGE ?? "1Gi",
    requests: { cpu: "50m", memory: "64Mi" },
    limits: { cpu: "500m", memory: "512Mi" },
    allowedEgressCidrs: ["100.64.0.0/10"],
    allowDns: false,
    secretProjections: [],
    provenance: { image: "golive:AWP_WORKSPACE_IMAGE", environment: "golive-native-fixture" },
  };
}

interface SelfProjectBootstrap {
  readonly repositoryUrl: string;
  readonly visionFile: string;
}

async function bootstrapSelfProject(
  lifecycle: I1LifecycleService,
  input: SelfProjectBootstrap,
): Promise<void> {
  const projects = await lifecycle.listProjects();
  let project = projects.find((candidate) => candidate.repositoryUrl === input.repositoryUrl);
  if (!project) {
    if (projects.length > 0) {
      throw new Error(
        "Self Project bootstrap requires a fresh database when AWP is not already registered",
      );
    }
    project = await lifecycle.createProject({
      name: "AWP",
      repositoryUrl: input.repositoryUrl,
      context: ownerMutationContext(),
    });
  }
  const summary = (await readFile(input.visionFile, "utf8")).trim();
  const hierarchy = await lifecycle.hierarchy(project.id);
  if (hierarchy?.vision?.summary !== summary) {
    await lifecycle.setProjectVision(project.id, summary, ownerMutationContext(project.id));
  }
}

export async function startControlPlane(input: {
  readonly databaseUrl: string;
  readonly port?: number;
  readonly migrationsFolder?: string;
  readonly selfProject?: SelfProjectBootstrap;
}): Promise<RunningControlPlane> {
  const migrationsFolder =
    input.migrationsFolder ??
    fileURLToPath(new URL("../../../packages/persistence/drizzle/", import.meta.url));
  const runtime = createPostgresRuntime(input.databaseUrl, migrationsFolder);
  await runtime.migrate();

  const subrouterUrl = process.env.AWP_SUBROUTER_URL;
  const subrouterAdminTokenFile = process.env.AWP_SUBROUTER_ADMIN_TOKEN_FILE;
  const subrouterAccountImportTokenFile = process.env.AWP_SUBROUTER_ACCOUNT_IMPORT_TOKEN_FILE;
  if (
    (subrouterUrl === undefined) !== (subrouterAdminTokenFile === undefined) ||
    (subrouterUrl === undefined) !== (subrouterAccountImportTokenFile === undefined)
  ) {
    throw new Error(
      "AWP_SUBROUTER_URL, AWP_SUBROUTER_ADMIN_TOKEN_FILE and AWP_SUBROUTER_ACCOUNT_IMPORT_TOKEN_FILE must be configured together",
    );
  }
  const subrouterClient =
    subrouterUrl === undefined ||
    subrouterAdminTokenFile === undefined ||
    subrouterAccountImportTokenFile === undefined
      ? undefined
      : new SubrouterHttpClient({
          baseUrl: subrouterUrl,
          adminToken: (await readFile(subrouterAdminTokenFile, "utf8")).trim(),
          accountImportToken: (await readFile(subrouterAccountImportTokenFile, "utf8")).trim(),
        });
  const accountProvider =
    subrouterClient === undefined ? undefined : new SubrouterAccountProvider(subrouterClient);
  const accountLogin =
    subrouterClient === undefined
      ? undefined
      : new AccountLoginManager(subrouterClient, process.env.AWP_CODEX_COMMAND ?? "codex");
  const accountProviderContext =
    accountProvider === undefined
      ? undefined
      : () => ({
          ...ownerMutationContext(),
          connectionId: unsafeOpaqueId<ConnectionId>("connection:subrouter-k3s"),
          credentialReferenceId: unsafeOpaqueId<CredentialReferenceId>(
            "credential:subrouter-k3s-admin",
          ),
        });

  const fabroUrl = process.env.AWP_FABRO_URL;
  const fabroTokenFile = process.env.AWP_FABRO_TOKEN_FILE;
  if ((fabroUrl === undefined) !== (fabroTokenFile === undefined)) {
    throw new Error("AWP_FABRO_URL and AWP_FABRO_TOKEN_FILE must be configured together");
  }
  const factoryProvider =
    fabroUrl === undefined || fabroTokenFile === undefined
      ? undefined
      : new FabroFactoryProvider(
          new FabroHttpClient({
            baseUrl: fabroUrl,
            bearerToken: (await readFile(fabroTokenFile, "utf8")).trim(),
          }),
        );

  const kubernetesApiBase = process.env.AWP_KUBERNETES_API_BASE;
  const callbackSecret = process.env.AWP_EXECUTION_CALLBACK_SECRET;
  if (kubernetesApiBase !== undefined && callbackSecret === undefined) {
    throw new Error("AWP_EXECUTION_CALLBACK_SECRET is required with Kubernetes execution");
  }
  const callbackBaseUrl =
    process.env.AWP_EXECUTION_CALLBACK_BASE_URL ?? `http://127.0.0.1:${input.port ?? 8787}`;
  const nativeAcpEnabled = process.env.AWP_NATIVE_ACP_ENABLED === "1";
  const modelGatewayUrl = process.env.AWP_MODEL_GATEWAY_URL;
  const modelGatewaySigningSecretFile = process.env.AWP_MODEL_GATEWAY_SIGNING_SECRET_FILE;
  if (
    nativeAcpEnabled &&
    (kubernetesApiBase === undefined ||
      callbackSecret === undefined ||
      modelGatewayUrl === undefined ||
      modelGatewaySigningSecretFile === undefined)
  ) {
    throw new Error(
      "Native ACP requires Kubernetes execution, callback authority, AWP_MODEL_GATEWAY_URL, and AWP_MODEL_GATEWAY_SIGNING_SECRET_FILE",
    );
  }
  const trustedRepositoryPath = process.env.AWP_TRUSTED_REPOSITORY_PATH;
  const trustedMerger =
    trustedRepositoryPath === undefined
      ? undefined
      : new LocalGitTrustedMergeAdapter(
          trustedRepositoryPath,
          process.env.AWP_CONTROL_PLANE_GIT_NAME ?? "AWP Control Plane",
          process.env.AWP_CONTROL_PLANE_GIT_EMAIL ?? "control-plane@awp.local",
        );
  const agentProvider =
    nativeAcpEnabled &&
    kubernetesApiBase !== undefined &&
    callbackSecret !== undefined &&
    modelGatewayUrl !== undefined &&
    modelGatewaySigningSecretFile !== undefined
      ? new AcpAgentProvider(
          new KubernetesAcpNativeClient(runtime.uow, {
            namespace: process.env.AWP_WORKSPACE_NAMESPACE ?? "awp-workspaces",
            callbackBaseUrl,
            callbackSecret,
            modelGatewayBaseUrl: modelGatewayUrl,
            modelGatewaySigningSecret: (
              await readFile(modelGatewaySigningSecretFile, "utf8")
            ).trim(),
            ...(trustedRepositoryPath === undefined ? {} : { trustedRepositoryPath }),
            ...(process.env.AWP_SELF_REPOSITORY_URL === undefined
              ? {}
              : { selfRepositoryUrl: process.env.AWP_SELF_REPOSITORY_URL }),
            ...(process.env.AWP_KUBECTL_COMMAND === undefined
              ? {}
              : { kubectlCommand: process.env.AWP_KUBECTL_COMMAND }),
          }),
          attemptRepository(runtime),
        )
      : undefined;

  const workspaceExecution =
    kubernetesApiBase === undefined
      ? undefined
      : new WorkspaceExecutionDispatcher(
          runtime.uow,
          new KubernetesWorkspaceProvider(
            new KubernetesApiTransport({
              apiBase: kubernetesApiBase,
              bearerToken: process.env.AWP_KUBERNETES_TOKEN ?? "",
            }),
            new WorkspaceProfileRegistry([workspaceProfile(process.env)]),
          ),
          ids,
          clock,
          {
            profileKey: "golive-native",
            callbackBaseUrl,
            callbackSecret: callbackSecret!,
            agentProviderId: unsafeOpaqueId<ProviderId>("provider:acp"),
            ...(process.env.AWP_AGENT_ACCOUNT_ID === undefined
              ? {}
              : { accountId: process.env.AWP_AGENT_ACCOUNT_ID }),
            ...(process.env.AWP_AGENT_MODEL === undefined
              ? {}
              : { model: process.env.AWP_AGENT_MODEL }),
            connectionId: unsafeOpaqueId<ConnectionId>("connection:kubernetes"),
            credentialReferenceId: unsafeOpaqueId<CredentialReferenceId>(
              "credential:kubernetes-runtime",
            ),
            forceFirstAttemptFailure: process.env.AWP_FORCE_FIRST_ATTEMPT_FAILURE === "1",
            ...(agentProvider === undefined
              ? {}
              : {
                  agentProvider,
                  agentConnectionId: unsafeOpaqueId<ConnectionId>("connection:acp-kubernetes"),
                  agentCredentialReferenceId: unsafeOpaqueId<CredentialReferenceId>(
                    "credential:acp-attempt-scoped",
                  ),
                }),
          },
          trustedMerger,
        );

  let dbosRuntime: Awaited<ReturnType<typeof startNativeDbosProvider>> | undefined;
  let executionDispatcher: ExecutionDispatcher | undefined = workspaceExecution;
  if (workspaceExecution && factoryProvider) {
    const worker = new ProviderExecutionWorker(runtime.uow, factoryProvider, workspaceExecution, {
      factoryConnectionId: unsafeOpaqueId<ConnectionId>("connection:fabro"),
      factoryCredentialReferenceId: unsafeOpaqueId<CredentialReferenceId>(
        "credential:fabro-control",
      ),
    });
    dbosRuntime = await startNativeDbosProvider({
      databaseUrl: input.databaseUrl,
      workflowKind: I1_DURABLE_DISPATCH_WORKFLOW_KIND,
      handler: worker,
      applicationName: "awp-control-plane",
      executorId: process.env.AWP_DBOS_EXECUTOR_ID ?? "awp-control-plane-dogfood",
    });
    executionDispatcher = new DurableExecutionDispatcher(dbosRuntime.provider, {
      workflowConnectionId: unsafeOpaqueId<ConnectionId>("connection:dbos"),
      workflowCredentialReferenceId:
        unsafeOpaqueId<CredentialReferenceId>("credential:dbos-control"),
    });
  }

  const lifecycle = new I1LifecycleService(runtime.uow, ids, clock, executionDispatcher);
  if (input.selfProject) await bootstrapSelfProject(lifecycle, input.selfProject);
  const app = createControlPlaneApp({
    lifecycle,
    ...(workspaceExecution === undefined ? {} : { execution: workspaceExecution }),
    ...(accountProvider === undefined || accountProviderContext === undefined
      ? {}
      : {
          accountProvider,
          accountProviderContext,
          ...(accountLogin === undefined ? {} : { accountLogin }),
        }),
    health: async () => {
      const database = await runtime.health();
      const migrations = await runtime.migrationStatus();
      return {
        ...database,
        status: migrations.pending === 0 && migrations.drift.length === 0 ? "ok" : "error",
        migrations,
      };
    },
  });
  const port = input.port ?? 8787;
  const server = serve({ fetch: app.fetch, port });
  const address = server.address();
  const boundPort = typeof address === "object" && address !== null ? address.port : port;
  if (boundPort === 0) {
    throw new Error("Control-plane server did not expose its bound port");
  }
  return {
    port: boundPort,
    runtime,
    server,
    async close(): Promise<void> {
      await new Promise<void>((resolve, reject) =>
        server.close((error) => (error ? reject(error) : resolve())),
      );
      if (dbosRuntime) await dbosRuntime.close();
      await runtime.close();
    },
  };
}

async function main(): Promise<void> {
  const databaseUrl = process.env.DATABASE_URL;
  if (!databaseUrl) throw new Error("DATABASE_URL is required");
  const port = process.env.PORT ? Number(process.env.PORT) : 8787;
  if (!Number.isInteger(port) || port <= 0 || port > 65535)
    throw new Error("PORT must be a valid TCP port");
  const selfRepositoryUrl = process.env.AWP_SELF_REPOSITORY_URL;
  const running = await startControlPlane({
    databaseUrl,
    port,
    ...(selfRepositoryUrl === undefined
      ? {}
      : {
          selfProject: {
            repositoryUrl: selfRepositoryUrl,
            visionFile:
              process.env.AWP_VISION_FILE ??
              fileURLToPath(new URL("../../../docs/VISION.md", import.meta.url)),
          },
        }),
  });
  process.stdout.write(`AWP control plane listening on http://127.0.0.1:${running.port}\n`);
  let closing = false;
  const close = async (): Promise<void> => {
    if (closing) return;
    closing = true;
    await running.close();
  };
  process.once("SIGINT", () => void close().then(() => process.exit(0)));
  process.once("SIGTERM", () => void close().then(() => process.exit(0)));
}

if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
  void main().catch((error) => {
    process.stderr.write(
      `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
    );
    process.exitCode = 1;
  });
}
