import { execFile as execFileCb } from "node:child_process";
import { resolve } from "node:path";
import type { SessionState } from "./session-types.js";
import { containerManager } from "./container-manager.js";
import { shellEscapeArg } from "./shell-utils.js";

/** Async wrapper around child_process.execFile that returns stdout as a string. */
function execFileAsync(
  file: string,
  args: string[],
  options: { cwd?: string; timeout?: number },
): Promise<string> {
  return new Promise((resolve, reject) => {
    execFileCb(file, args, { ...options, encoding: "utf-8" }, (error, stdout) => {
      if (error) reject(error);
      else resolve(String(stdout));
    });
  });
}

async function runGitCommand(sessionId: string, state: SessionState, gitArgs: string[]): Promise<string> {
  if (state.is_containerized) {
    const container = containerManager.getContainer(sessionId);
    if (container?.containerId) {
      const containerCwd = container.containerCwd || "/workspace";
      const escapedArgs = gitArgs.map((a) => shellEscapeArg(a)).join(" ");
      const inner = `cd ${shellEscapeArg(containerCwd)} && git ${escapedArgs}`;
      return (
        await execFileAsync("docker", ["exec", container.containerId, "sh", "-lc", inner], {
          timeout: 3000,
        })
      ).trim();
    }
    throw new Error("container not tracked");
  }

  return (
    await execFileAsync("git", gitArgs, {
      cwd: state.cwd,
      timeout: 3000,
    })
  ).trim();
}

function mapContainerPathToHost(sessionId: string, state: SessionState, pathValue: string): string {
  if (!state.is_containerized || !pathValue) return pathValue;
  const container = containerManager.getContainer(sessionId);
  const containerCwd = (container?.containerCwd || "/workspace").replace(/\/+$/, "") || "/";
  const hostCwd = (container?.hostCwd || state.cwd || "").replace(/\/+$/, "") || "/";

  if (pathValue === containerCwd) return hostCwd;
  if (containerCwd !== "/" && pathValue.startsWith(`${containerCwd}/`)) {
    return `${hostCwd}${pathValue.slice(containerCwd.length)}`;
  }
  return pathValue;
}

// ── TTL Cache (5 seconds) ──────────────────────────────────────────────────

const GIT_INFO_TTL_MS = 5_000;

interface GitInfoCacheEntry {
  expiresAt: number;
  promise: Promise<void>;
}

const gitInfoCache = new Map<string, GitInfoCacheEntry>();

/** Evict expired entries to prevent unbounded growth. */
function purgeGitInfoCache(): void {
  const now = Date.now();
  for (const [key, entry] of gitInfoCache) {
    if (now >= entry.expiresAt) gitInfoCache.delete(key);
  }
}

/** Exported for testing only. */
export function clearGitInfoCache(): void {
  gitInfoCache.clear();
}

export async function resolveSessionGitInfo(sessionId: string, state: SessionState): Promise<void> {
  if (!state.cwd) return;

  // Return cached result if still fresh (avoids redundant re-resolution
  // when multiple browser tabs connect simultaneously).
  const cached = gitInfoCache.get(sessionId);
  if (cached && Date.now() < cached.expiresAt) {
    return cached.promise;
  }

  const promise = resolveSessionGitInfoUncached(sessionId, state);
  gitInfoCache.set(sessionId, { expiresAt: Date.now() + GIT_INFO_TTL_MS, promise });

  // Periodically trim stale entries
  if (gitInfoCache.size > 50) purgeGitInfoCache();

  return promise;
}

async function resolveSessionGitInfoUncached(sessionId: string, state: SessionState): Promise<void> {
  const wasContainerized = state.is_containerized;
  const previous = {
    git_branch: state.git_branch,
    is_worktree: state.is_worktree,
    repo_root: state.repo_root,
    git_ahead: state.git_ahead,
    git_behind: state.git_behind,
  };
  try {
    // Run the 3 independent git commands in parallel
    const [branch, gitDir, showToplevel] = await Promise.all([
      runGitCommand(sessionId, state, ["rev-parse", "--abbrev-ref", "HEAD"]),
      runGitCommand(sessionId, state, ["rev-parse", "--git-dir"]).catch(() => null),
      runGitCommand(sessionId, state, ["rev-parse", "--show-toplevel"]).catch(() => null),
    ]);

    state.git_branch = branch;

    // Determine worktree status from git-dir result
    state.is_worktree = gitDir != null && /[\/\\]worktrees[\/\\]/.test(gitDir);

    // Resolve repo root — worktrees need an extra command for the common dir
    try {
      if (state.is_worktree) {
        const commonDir = await runGitCommand(sessionId, state, ["rev-parse", "--git-common-dir"]);
        state.repo_root = resolve(state.cwd!, commonDir, "..");
      } else if (showToplevel != null) {
        state.repo_root = showToplevel;
      }
      state.repo_root = mapContainerPathToHost(sessionId, state, state.repo_root ?? "");
    } catch {
      // Ignore repo root resolution failures
    }

    try {
      const counts = await runGitCommand(
        sessionId,
        state,
        ["rev-list", "--left-right", "--count", "@{upstream}...HEAD"],
      );
      const [behind, ahead] = counts.split(/\s+/).map(Number);
      state.git_ahead = ahead || 0;
      state.git_behind = behind || 0;
    } catch {
      state.git_ahead = 0;
      state.git_behind = 0;
    }
  } catch (error) {
    if (state.is_containerized && error instanceof Error && error.message === "container not tracked") {
      state.git_branch = previous.git_branch;
      state.is_worktree = previous.is_worktree;
      state.repo_root = previous.repo_root;
      state.git_ahead = previous.git_ahead;
      state.git_behind = previous.git_behind;
      state.is_containerized = wasContainerized;
      return;
    }
    state.git_branch = "";
    state.is_worktree = false;
    state.repo_root = "";
    state.git_ahead = 0;
    state.git_behind = 0;
  }
  state.is_containerized = wasContainerized;
}
