import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir, hostname } from "node:os";
import { dirname, join, resolve } from "node:path";
import { DEFAULT_RELAY_URL, RELAY_TOKEN_HEADER, errMessage, stringValue } from "agent-relay-sdk";
import type { SettingEntry, WorkspaceMetadata } from "agent-relay-sdk";
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
import { getManifest } from "agent-relay-providers";

interface ProviderConfig {
  command: string;
  defaultArgs: string[];
  env: Record<string, string>;
  pluginDirs: string[];
  globalInstructionFiles?: string[];
  defaultCapabilities: string[];
  defaultApprovalMode: string;
  defaultTags: string[];
  chatCaptureMode: "final" | "full";
  reasoningCapture?: boolean;
  headless: {
    tmuxPrefix: string;
    shutdownTimeoutMs: number;
  };
}

interface GlobalRunnerConfig {
  relayUrl: string;
  token?: string;
  defaultCwd: string;
}

interface LoadedProviderConfig extends ProviderConfig {
  path: string;
}

export function agentRelayHome(): string {
  return process.env.AGENT_RELAY_HOME || join(homedir(), ".agent-relay");
}

function providersDir(home = agentRelayHome()): string {
  return join(home, "providers");
}

const DEFAULT_PROVIDER_ARGS: Record<string, string[]> = {
  claude: ["--dangerously-skip-permissions"],
};

export function defaultProviderConfig(provider: string): ProviderConfig {
  const manifest = getManifest(provider);
  const home = recordValue(manifest?.home);
  return {
    command: manifest?.probe?.command ?? provider,
    defaultArgs: DEFAULT_PROVIDER_ARGS[provider] ?? [],
    env: {},
    pluginDirs: [],
    globalInstructionFiles: stringArray(home.globalInstructionFiles) ?? [],
    defaultCapabilities: ["chat", "code", "review"],
    defaultApprovalMode: "guarded",
    defaultTags: [],
    chatCaptureMode: "final",
    reasoningCapture: true,
    headless: {
      tmuxPrefix: `${provider}-relay`,
      shutdownTimeoutMs: 10_000,
    },
  };
}

export function loadGlobalConfig(home = agentRelayHome()): GlobalRunnerConfig {
  const path = join(home, "config.json");
  const parsed = readJson(path);
  return {
    relayUrl: stringValue(parsed.relayUrl) ?? relayUrlFromEnv() ?? DEFAULT_RELAY_URL,
    token: stringValue(parsed.token) ?? relayTokenFromEnv(),
    defaultCwd: stringValue(parsed.defaultCwd) ?? process.cwd(),
  };
}

function relayUrlFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_URL;
}

function textEnvOrFile(name: string): string | undefined {
  const inline = process.env[name];
  if (inline) return inline;
  const file = process.env[`${name}_FILE`];
  if (!file) return undefined;
  try {
    const text = readFileSync(file, "utf8");
    return text.length > 0 ? text : undefined;
  } catch {
    return undefined;
  }
}

export function relayTokenFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_TOKEN;
}

export function runtimeTokenProfileFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_TOKEN_PROFILE;
}

export function runtimeTokenJtiFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_TOKEN_JTI;
}

export function runtimeTokenExpiresAtFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_TOKEN_EXPIRES_AT;
}

// Deadline for a freshly spawned runner's INITIAL relay registration. Reconnects after the
// first registration retry forever (a long-lived agent rides out a relay blip), but the
// initial handshake is bounded: a runner that can never register (a token the relay rejects,
// an unreachable relay) must EXIT instead of hanging `active` forever — a hung runner is
// invisible to the orchestrator's exit detection, so the spawn-parent is never notified.
export function registrationTimeoutMsFromEnv(): number {
  const parsed = Number(process.env.AGENT_RELAY_REGISTRATION_TIMEOUT_MS);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 60_000;
}

export function nativeSelfResumeTimeoutMsFromEnv(): number {
  const parsed = Number(process.env.AGENT_RELAY_NATIVE_SELF_RESUME_TIMEOUT_MS);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 120_000;
}

export function agentProfileNameFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_AGENT_PROFILE;
}

export function agentProfileJsonFromEnv(): string | undefined {
  return textEnvOrFile("AGENT_RELAY_AGENT_PROFILE_JSON");
}

export function workspaceJsonFromEnv(): string | undefined {
  return textEnvOrFile("AGENT_RELAY_WORKSPACE_JSON");
}

export function relayInjectionEventsJsonFromEnv(): string | undefined {
  return textEnvOrFile("AGENT_RELAY_INJECTION_EVENTS_JSON");
}

export function promptFromEnv(): string | undefined {
  return textEnvOrFile("AGENT_RELAY_PROMPT");
}

export function systemPromptAppendFromEnv(): string | undefined {
  return textEnvOrFile("AGENT_RELAY_SYSTEM_PROMPT_APPEND");
}

export function tmuxSessionFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_TMUX_SESSION;
}

export function policyNameFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_POLICY;
}

export function spawnRequestIdFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_SPAWN_REQUEST_ID;
}

export function taskIdFromEnv(): number | undefined {
  const parsed = Number(process.env.AGENT_RELAY_TASK_ID);
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
}

export function automationIdFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_AUTOMATION_ID;
}

export function automationRunIdFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_AUTOMATION_RUN_ID;
}

export function lifecycleFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_LIFECYCLE;
}

export function sessionDebugEnabled(): boolean {
  return process.env.AGENT_RELAY_SESSION_DEBUG === "1";
}

export function logLevelFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_LOG_LEVEL;
}

export function mcpProxyEnabledFromEnv(): boolean {
  return !["0", "false", "off"].includes((process.env.AGENT_RELAY_MCP_PROXY ?? "").trim().toLowerCase());
}

export function runnerInfoFileFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_RUNNER_INFO_FILE;
}

export function runnerOutboxDirFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_RUNNER_OUTBOX_DIR;
}

export function runnerOutboxDirWithInfoFallback(): string | undefined {
  const dir = runnerOutboxDirFromEnv();
  if (dir) return dir;
  const infoFile = runnerInfoFileFromEnv();
  return infoFile ? join(dirname(infoFile), "outbox") : undefined;
}

export function capsFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_CAPS;
}

export function tagsFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_TAGS;
}

export function workspaceModeFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_WORKSPACE_MODE;
}

export function orchestratorBaseDirFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_ORCHESTRATOR_BASE_DIR;
}

export function runnerLogFileFromEnv(): string | undefined {
  return typeof process.env.AGENT_RELAY_LOG_FILE === "string" ? process.env.AGENT_RELAY_LOG_FILE : undefined;
}

export function orchestratorUrlFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_ORCHESTRATOR_URL;
}

export function contextStateDirFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_CONTEXT_STATE_DIR;
}

export function attachmentCacheDirFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_ATTACHMENT_CACHE_DIR;
}

export function providerHomeRootFromEnv(): string {
  return process.env.AGENT_RELAY_PROVIDER_HOME_ROOT || join(homedir(), ".agent-relay", "provider-homes");
}

export function messageBodyMaxCharsFromEnv(): string | undefined {
  return process.env.AGENT_RELAY_MESSAGE_BODY_MAX_CHARS;
}

export function loadProviderConfig(provider: string, home = agentRelayHome()): LoadedProviderConfig {
  const path = join(providersDir(home), `${provider}.json`);
  const raw = readJson(path);
  const defaults = defaultProviderConfig(provider);
  return {
    path,
    command: stringValue(raw.command) ?? defaults.command,
    defaultArgs: stringArray(raw.defaultArgs) ?? defaults.defaultArgs,
    env: stringRecord(raw.env) ?? defaults.env,
    pluginDirs: stringArray(raw.pluginDirs) ?? defaults.pluginDirs,
    globalInstructionFiles: stringArray(raw.globalInstructionFiles) ?? defaults.globalInstructionFiles,
    defaultCapabilities: stringArray(raw.defaultCapabilities) ?? defaults.defaultCapabilities,
    defaultApprovalMode: stringValue(raw.defaultApprovalMode) ?? defaults.defaultApprovalMode,
    defaultTags: stringArray(raw.defaultTags) ?? defaults.defaultTags,
    chatCaptureMode: enumValue(raw.chatCaptureMode, ["final", "full"]) ?? defaults.chatCaptureMode,
    reasoningCapture: booleanValue(raw.reasoningCapture) ?? defaults.reasoningCapture,
    headless: {
      tmuxPrefix: stringValue(recordValue(raw.headless).tmuxPrefix) ?? defaults.headless.tmuxPrefix,
      shutdownTimeoutMs: positiveInteger(recordValue(raw.headless).shutdownTimeoutMs) ?? defaults.headless.shutdownTimeoutMs,
    },
  };
}

export async function loadProviderConfigFromRelay(
  provider: string,
  opts: { relayUrl: string; token?: string; home?: string; host?: string },
): Promise<LoadedProviderConfig> {
  const host = opts.host ?? hostname();
  const home = opts.home ?? agentRelayHome();
  const local = loadProviderConfig(provider, home);
  const key = `${host}/${provider}`;

  try {
    const fetched = await fetchProviderConfig(provider, opts.relayUrl, opts.token, key);
    if (fetched) return fetched;
  } catch (error) {
    console.warn(`[agent-relay] provider config relay read failed (${errMessage(error)}); using local fallback ${local.path}.`);
  }
  return local;
}

export function writeProviderConfig(provider: string, config: ProviderConfig, home = agentRelayHome()): LoadedProviderConfig {
  const dir = providersDir(home);
  mkdirSync(dir, { recursive: true });
  const path = join(dir, `${provider}.json`);
  writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
  return { ...config, path };
}

export function providerConfigPublic(config: LoadedProviderConfig): Record<string, unknown> {
  return {
    path: config.path,
    command: config.command,
    defaultArgs: config.defaultArgs,
    env: maskEnv(config.env),
    pluginDirs: config.pluginDirs,
    globalInstructionFiles: config.globalInstructionFiles ?? [],
    defaultCapabilities: config.defaultCapabilities,
    defaultApprovalMode: config.defaultApprovalMode,
    defaultTags: config.defaultTags,
    chatCaptureMode: config.chatCaptureMode,
    reasoningCapture: config.reasoningCapture,
    headless: config.headless,
  };
}

export function runnerId(provider: string, cwd: string, label?: string): string {
  const project = cwd.split("/").filter(Boolean).at(-1) || "workspace";
  const cleanLabel = sanitizeFsName(label || project, { replacement: "-", lowercase: true });
  return `${hostname()}-${provider}-${cleanLabel}-${crypto.randomUUID().slice(0, 8)}`;
}

/**
 * Stable project identifier for insights aggregation: the repo NAME, never a
 * per-branch/per-session worktree dir or full path. Isolated workspace agents run
 * from a session-specific worktree (…/workspaces/<repo>/<id>) whose basename is
 * unique per session — using it would scatter one repo's data across many "projects"
 * and break per-project rollups. So we resolve up to the main repo root, then take
 * its basename. Falls back to the git toplevel of cwd (handles a direct agent
 * launched in a subdir), then to the cwd basename for a non-git directory.
 */
export function resolveProjectName(cwd: string, workspace?: WorkspaceMetadata): string {
  const root =
    workspace?.repoRoot ||
    workspace?.probe?.repoRoot ||
    gitToplevel(cwd) ||
    workspace?.sourceCwd ||
    cwd;
  return root.split("/").filter(Boolean).at(-1) || "unknown";
}

function gitToplevel(cwd: string): string | undefined {
  try {
    const out = execFileSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
    }).trim();
    return out || undefined;
  } catch {
    return undefined;
  }
}

export function resolveCwd(value: string | undefined, fallback: string): string {
  return resolve(value || fallback);
}

function readJson(path: string): Record<string, unknown> {
  if (!existsSync(path)) return {};
  try {
    const parsed = JSON.parse(readFileSync(path, "utf8"));
    return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
  } catch {
    return {};
  }
}

function positiveInteger(value: unknown): number | undefined {
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
}

function enumValue<T extends string>(value: unknown, allowed: T[]): T | undefined {
  return typeof value === "string" && allowed.includes(value as T) ? (value as T) : undefined;
}

function booleanValue(value: unknown): boolean | undefined {
  return typeof value === "boolean" ? value : undefined;
}

function stringArray(value: unknown): string[] | undefined {
  return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : undefined;
}

function stringRecord(value: unknown): Record<string, string> | undefined {
  if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
  const entries = Object.entries(value);
  if (entries.some(([, item]) => typeof item !== "string")) return undefined;
  return Object.fromEntries(entries) as Record<string, string>;
}

function recordValue(value: unknown): Record<string, unknown> {
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}

function maskEnv(env: Record<string, string>): Record<string, string> {
  const result: Record<string, string> = {};
  for (const [key, value] of Object.entries(env)) {
    result[key] = /token|secret|key|password/i.test(key) && !value.startsWith("$env:") ? "********" : value;
  }
  return result;
}

async function fetchProviderConfig(
  provider: string,
  relayUrl: string,
  token: string | undefined,
  key: string,
): Promise<LoadedProviderConfig | null> {
  const url = new URL(`/api/settings/provider-config/${encodeURIComponent(key)}`, relayUrl);
  const res = await fetch(url, {
    headers: token ? { [RELAY_TOKEN_HEADER]: token } : undefined,
    signal: AbortSignal.timeout(5_000),
  });
  if (res.status === 404) return null;
  const body = await res.json().catch(() => null) as (Partial<SettingEntry> & { error?: string }) | null;
  if (!res.ok) throw new Error(body?.error ? `${res.status}: ${body.error}` : `HTTP ${res.status}`);
  if (body?.version === 0) return null;
  return providerConfigFromValue(provider, body?.value, `config:provider-config/${key}`);
}

function providerConfigFromValue(provider: string, value: unknown, path: string): LoadedProviderConfig {
  const raw = recordValue(value);
  const defaults = defaultProviderConfig(provider);
  return {
    path,
    command: stringValue(raw.command) ?? defaults.command,
    defaultArgs: stringArray(raw.defaultArgs) ?? defaults.defaultArgs,
    env: stringRecord(raw.env) ?? defaults.env,
    pluginDirs: stringArray(raw.pluginDirs) ?? defaults.pluginDirs,
    globalInstructionFiles: stringArray(raw.globalInstructionFiles) ?? defaults.globalInstructionFiles,
    defaultCapabilities: stringArray(raw.defaultCapabilities) ?? defaults.defaultCapabilities,
    defaultApprovalMode: stringValue(raw.defaultApprovalMode) ?? defaults.defaultApprovalMode,
    defaultTags: stringArray(raw.defaultTags) ?? defaults.defaultTags,
    chatCaptureMode: enumValue(raw.chatCaptureMode, ["final", "full"]) ?? defaults.chatCaptureMode,
    reasoningCapture: booleanValue(raw.reasoningCapture) ?? defaults.reasoningCapture,
    headless: {
      tmuxPrefix: stringValue(recordValue(raw.headless).tmuxPrefix) ?? defaults.headless.tmuxPrefix,
      shutdownTimeoutMs: positiveInteger(recordValue(raw.headless).shutdownTimeoutMs) ?? defaults.headless.shutdownTimeoutMs,
    },
  };
}
