import type { CliLauncher, SdkSessionInfo } from "./cli-launcher.js";
import type { WsBridge } from "./ws-bridge.js";
import type { WorktreeTracker } from "./worktree-tracker.js";
import type { ContainerInfo, ContainerConfig } from "./container-manager.js";
import type { CreationStepId, LaunchOptions } from "./session-types.js";
import { containerManager } from "./container-manager.js";
import { hasContainerClaudeAuth } from "./claude-container-auth.js";
import { hasContainerCodexAuth } from "./codex-container-auth.js";
import { imagePullManager } from "./image-pull-manager.js";
import * as envManager from "./env-manager.js";
import * as gitUtils from "./git-utils.js";
import { createLogger } from "./logger.js";
import { config } from "./config.js";
import { sessionCreationErrorMessage } from "./user-error-messages.js";

const log = createLogger("session-creator");

// ─── Constants ───────────────────────────────────────────────────────────────

const VSCODE_EDITOR_CONTAINER_PORT = 13337;

// ─── Error type ──────────────────────────────────────────────────────────────

/**
 * Structured error thrown during session creation.
 * Contains an HTTP status code and optional creation step for SSE error events.
 */
export class SessionCreationError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number,
    public readonly step?: CreationStepId,
  ) {
    super(message);
    this.name = "SessionCreationError";
  }
}

// ─── Callback & dependency types ─────────────────────────────────────────────

/**
 * Progress callback invoked at each creation step.
 * SSE routes hook this to emit progress events; sync routes pass nothing.
 */
export type ProgressCallback = (
  step: CreationStepId,
  label: string,
  status: "in_progress" | "done" | "error",
  detail?: string,
) => void | Promise<void>;

export interface SessionCreationDeps {
  launcher: CliLauncher;
  wsBridge: WsBridge;
  worktreeTracker: WorktreeTracker;
}

// ─── Shared session creation logic ───────────────────────────────────────────

/**
 * Core session creation logic shared by both the sync and SSE endpoints.
 *
 * Throws {@link SessionCreationError} for expected failures (invalid input,
 * container issues, etc.). The caller is responsible for formatting the error
 * into the appropriate HTTP/SSE response.
 */
export async function createSession(
  body: Record<string, unknown>,
  deps: SessionCreationDeps,
  onProgress?: ProgressCallback,
): Promise<SdkSessionInfo> {
  const { launcher, wsBridge, worktreeTracker } = deps;

  const resumeSessionAt =
    typeof body.resumeSessionAt === "string" && body.resumeSessionAt.trim()
      ? body.resumeSessionAt.trim()
      : undefined;
  const forkSession = body.forkSession === true;
  const backend = (body.backend as string) ?? "claude";

  if (backend !== "claude" && backend !== "codex") {
    throw new SessionCreationError(
      `Invalid backend: ${String(backend)}`,
      400,
    );
  }

  // ── Step: Resolve environment ──────────────────────────────────────────

  await onProgress?.("resolving_env", "Resolving environment...", "in_progress");

  let envVars: Record<string, string> | undefined = body.env as
    | Record<string, string>
    | undefined;
  const zencodeEnv = body.envSlug
    ? envManager.getEnv(body.envSlug as string)
    : null;

  if (body.envSlug && zencodeEnv) {
    log.info(
      { envName: zencodeEnv.name, varCount: Object.keys(zencodeEnv.variables).length, vars: Object.keys(zencodeEnv.variables).join(", ") },
      "Injecting environment variables",
    );
    envVars = { ...zencodeEnv.variables, ...(body.env as Record<string, string>) };
  } else if (body.envSlug) {
    log.warn(
      { envSlug: body.envSlug },
      "Environment not found, ignoring",
    );
  }

  await onProgress?.("resolving_env", "Environment resolved", "done");

  // ── Step: Git operations ───────────────────────────────────────────────

  let cwd = body.cwd as string | undefined;
  let worktreeInfo:
    | {
        isWorktree: boolean;
        repoRoot: string;
        branch: string;
        actualBranch: string;
        worktreePath: string;
      }
    | undefined;

  // Validate branch name to prevent command injection via shell metacharacters
  if (body.branch && !/^[a-zA-Z0-9/_.\-]+$/.test(body.branch as string)) {
    throw new SessionCreationError("Invalid branch name", 400, "checkout_branch");
  }

  if (body.useWorktree && body.branch && cwd) {
    await onProgress?.("creating_worktree", "Creating worktree...", "in_progress");

    const repoInfo = await gitUtils.getRepoInfo(cwd);
    if (repoInfo) {
      const result = await gitUtils.ensureWorktree(
        repoInfo.repoRoot,
        body.branch as string,
        {
          baseBranch: repoInfo.defaultBranch,
          createBranch: body.createBranch as boolean | undefined,
          forceNew: true,
        },
      );
      cwd = result.worktreePath;
      worktreeInfo = {
        isWorktree: true,
        repoRoot: repoInfo.repoRoot,
        branch: body.branch as string,
        actualBranch: result.actualBranch,
        worktreePath: result.worktreePath,
      };
    }

    await onProgress?.("creating_worktree", "Worktree ready", "done");
  } else if (body.branch && cwd) {
    const repoInfo = await gitUtils.getRepoInfo(cwd);
    if (repoInfo) {
      await onProgress?.("fetching_git", "Fetching from remote...", "in_progress");

      const fetchResult = await gitUtils.gitFetch(repoInfo.repoRoot);
      if (!fetchResult.success) {
        throw new SessionCreationError(
          `git fetch failed before session create: ${fetchResult.output}`,
          500,
          "fetching_git",
        );
      }

      await onProgress?.("fetching_git", "Fetch complete", "done");

      if (repoInfo.currentBranch !== (body.branch as string)) {
        await onProgress?.("checkout_branch", `Checking out ${body.branch}...`, "in_progress");

        await gitUtils.checkoutOrCreateBranch(
          repoInfo.repoRoot,
          body.branch as string,
          {
            createBranch: body.createBranch as boolean | undefined,
            defaultBranch: repoInfo.defaultBranch,
          },
        );

        await onProgress?.("checkout_branch", `On branch ${body.branch}`, "done");
      }

      await onProgress?.("pulling_git", "Pulling latest changes...", "in_progress");

      const pullResult = await gitUtils.gitPull(repoInfo.repoRoot);
      if (!pullResult.success) {
        log.warn(
          { repoRoot: repoInfo.repoRoot, output: pullResult.output },
          "git pull warning (non-fatal)",
        );
      }

      await onProgress?.("pulling_git", "Up to date", "done");
    }
  }

  // ── Step: Docker image resolution & container auth checks ──────────────

  const effectiveImage = zencodeEnv
    ? body.envSlug
      ? envManager.getEffectiveImage(body.envSlug as string)
      : null
    : ((body.container as { image?: string })?.image || null);

  let containerInfo: ContainerInfo | undefined;
  let containerId: string | undefined;
  let containerName: string | undefined;
  let containerImage: string | undefined;

  if (effectiveImage && backend === "claude" && !hasContainerClaudeAuth(envVars)) {
    throw new SessionCreationError(
      "Containerized Claude requires auth available inside the container. " +
        "Set ANTHROPIC_API_KEY (or ANTHROPIC_AUTH_TOKEN / CLAUDE_CODE_AUTH_TOKEN) in the selected environment.",
      400,
    );
  }
  if (effectiveImage && backend === "codex" && !hasContainerCodexAuth(envVars)) {
    throw new SessionCreationError(
      "Containerized Codex requires auth available inside the container. " +
        "Set OPENAI_API_KEY in the selected environment, or ensure ~/.codex/auth.json exists on the host.",
      400,
    );
  }

  // ── Step: Pull image & create container ────────────────────────────────

  if (effectiveImage) {
    if (!(await imagePullManager.isReady(effectiveImage))) {
      const pullState = await imagePullManager.getState(effectiveImage);
      if (pullState.status === "idle" || pullState.status === "error") {
        await imagePullManager.ensureImage(effectiveImage);
      }

      await onProgress?.("pulling_image", "Pulling Docker image...", "in_progress");

      // Stream pull progress lines when a progress callback is available
      const unsub = onProgress
        ? imagePullManager.onProgress(effectiveImage, (line) => {
            void onProgress("pulling_image", "Pulling Docker image...", "in_progress", line);
          })
        : undefined;

      const ready = await imagePullManager.waitForReady(effectiveImage, 300_000);
      unsub?.();

      if (ready) {
        await onProgress?.("pulling_image", "Image ready", "done");
      } else {
        const state = await imagePullManager.getState(effectiveImage);
        throw new SessionCreationError(
          state.error ||
            `Docker image ${effectiveImage} could not be pulled or built. Use the environment manager to pull/build the image first.`,
          503,
          "pulling_image",
        );
      }
    }

    // --- Create container ---
    await onProgress?.("creating_container", "Starting container...", "in_progress");

    const tempId = crypto.randomUUID().slice(0, 8);
    const requestedPorts =
      zencodeEnv?.ports ??
      (Array.isArray((body.container as { ports?: number[] })?.ports)
        ? (body.container as { ports: number[] }).ports
            .map(Number)
            .filter((n: number) => n > 0)
        : []);
    const containerPorts = Array.from(
      new Set([...requestedPorts, VSCODE_EDITOR_CONTAINER_PORT]),
    );
    const cConfig: ContainerConfig = {
      image: effectiveImage,
      ports: containerPorts,
      volumes: zencodeEnv?.volumes ?? (body.container as { volumes?: string[] })?.volumes,
      env: envVars,
    };

    try {
      containerInfo = await containerManager.createContainer(tempId, cwd!, cConfig);
    } catch (err) {
      const reason = err instanceof Error ? err.message : String(err);
      throw new SessionCreationError(
        `Docker is required to run this environment image (${effectiveImage}) ` +
          `but container startup failed: ${reason}`,
        503,
        "creating_container",
      );
    }

    containerId = containerInfo.containerId;
    containerName = containerInfo.name;
    containerImage = effectiveImage;

    await onProgress?.("creating_container", "Container running", "done");

    // --- Copy workspace into isolated volume ---
    await onProgress?.("copying_workspace", "Copying workspace files...", "in_progress");

    try {
      await containerManager.copyWorkspaceToContainer(
        containerInfo.containerId,
        cwd!,
      );
      await containerManager.reseedGitAuth(containerInfo.containerId);
      await onProgress?.("copying_workspace", "Workspace copied", "done");
    } catch (err) {
      await containerManager.removeContainer(tempId);
      const reason = err instanceof Error ? err.message : String(err);
      throw new SessionCreationError(
        `Failed to copy workspace to container: ${reason}`,
        503,
        "copying_workspace",
      );
    }

    // --- Init script ---
    if (zencodeEnv?.initScript?.trim()) {
      await onProgress?.("running_init_script", "Running init script...", "in_progress");

      try {
        log.info(
          { envName: zencodeEnv.name, containerName: containerInfo.name },
          "Running init script",
        );
        const initTimeout = config.initScriptTimeout;

        const result = await containerManager.execInContainerAsync(
          containerInfo.containerId,
          ["sh", "-lc", zencodeEnv.initScript],
          {
            timeout: initTimeout,
            onOutput: onProgress
              ? (line: string) => {
                  void onProgress(
                    "running_init_script",
                    "Running init script...",
                    "in_progress",
                    line,
                  );
                }
              : undefined,
          },
        );

        if (result.exitCode !== 0) {
          log.error(
            { envName: zencodeEnv.name, exitCode: result.exitCode, output: result.output },
            "Init script failed",
          );
          await containerManager.removeContainer(tempId);
          const truncated =
            result.output.length > 2000
              ? result.output.slice(0, 500) +
                "\n...[truncated]...\n" +
                result.output.slice(-1500)
              : result.output;
          throw new SessionCreationError(
            `Init script failed (exit ${result.exitCode}):\n${truncated}`,
            503,
            "running_init_script",
          );
        }

        log.info(
          { envName: zencodeEnv.name },
          "Init script completed successfully",
        );
        await onProgress?.("running_init_script", "Init script complete", "done");
      } catch (e) {
        if (e instanceof SessionCreationError) throw e;
        await containerManager.removeContainer(tempId);
        const reason = e instanceof Error ? e.message : String(e);
        throw new SessionCreationError(
          `Init script execution failed: ${reason}`,
          503,
          "running_init_script",
        );
      }
    }
  }

  // ── Step: Launch CLI ───────────────────────────────────────────────────

  await onProgress?.("launching_cli", "Launching Claude Code...", "in_progress");

  let session;
  try {
    session = await launcher.launch({
      model: body.model as string | undefined,
      permissionMode: body.permissionMode as string | undefined,
      cwd,
      claudeBinary: body.claudeBinary as string | undefined,
      codexBinary: body.codexBinary as string | undefined,
      codexInternetAccess: backend === "codex",
      codexSandbox:
        backend === "codex"
          ? (body.codexSandbox as string) === "danger-full-access" ||
            (body.codexSandbox as string) === "workspace-write"
            ? (body.codexSandbox as "danger-full-access" | "workspace-write")
            : (body.permissionMode as string) === "bypassPermissions"
              ? "danger-full-access"
              : "workspace-write"
          : undefined,
      allowedTools: body.allowedTools as string[] | undefined,
      env: envVars,
      backendType: backend,
      containerId,
      containerName,
      containerImage,
      containerCwd: containerInfo?.containerCwd,
      resumeSessionAt,
      forkSession,
      enableFileCheckpointing: body.enableFileCheckpointing as boolean | undefined,
      maxTurns: body.maxTurns as number | undefined,
      maxBudgetUsd: body.maxBudgetUsd as number | undefined,
      systemPrompt: body.systemPrompt as string | undefined,
      appendSystemPrompt: body.appendSystemPrompt as string | undefined,
      thinking: body.thinking as LaunchOptions["thinking"],
      effort: body.effort as LaunchOptions["effort"],
      disallowedTools: body.disallowedTools as string[] | undefined,
      additionalDirectories: body.additionalDirectories as string[] | undefined,
      betas: body.betas as string[] | undefined,
    });
  } catch (err) {
    // Clean up container if we created one but the CLI launch failed
    if (containerInfo) {
      await containerManager.removeContainer(containerInfo.containerId).catch((err) => {
        log.warn({ containerId: containerInfo.containerId, err }, "Failed to clean up container after CLI launch failure");
      });
    }
    const reason = err instanceof Error ? err.message : String(err);
    throw new SessionCreationError(
      sessionCreationErrorMessage(reason),
      503,
      "launching_cli",
    );
  }

  // Re-track container with real session ID
  if (containerInfo) {
    containerManager.retrack(containerInfo.containerId, session.sessionId);
    wsBridge.markContainerized(session.sessionId, cwd!);
  }

  // Store budget cap on session state for server-side enforcement
  if (body.maxBudgetUsd != null) {
    wsBridge.setBudget(session.sessionId, body.maxBudgetUsd as number);
  }

  // Track the worktree mapping
  if (worktreeInfo) {
    worktreeTracker.addMapping({
      sessionId: session.sessionId,
      repoRoot: worktreeInfo.repoRoot,
      branch: worktreeInfo.branch,
      actualBranch: worktreeInfo.actualBranch,
      worktreePath: worktreeInfo.worktreePath,
      createdAt: Date.now(),
    });
  }

  await onProgress?.("launching_cli", "Session started", "done");

  return session;
}
