/**
 * Harness-agnostic state resolution.
 *
 * The same extension runs under pi (`~/.pi/agent`) and under omp (`~/.omp/agent`), so the config file
 * has to land beside whichever harness loaded us. Resolution order:
 *
 *   1. an explicit env override (PI_CODING_AGENT_DIR / OMP_CODING_AGENT_DIR)
 *   2. the harness marker the process carries (AI_AGENT=pi|omp)
 *   3. the extension's own location when it was loaded from `<agentDir>/extensions/`
 *   4. the first existing default directory
 *   5. `~/.pi/agent`
 *
 * Everything is injectable so the logic is unit-testable without a harness.
 */

import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";

export type HarnessName = "pi" | "omp" | "unknown";

export interface HarnessDefaults {
  /** Harness marker value -> agent directory, relative to the home directory. */
  agentDirs: Record<string, string>;
  /** Env var carrying the harness marker. */
  markerEnv: string;
  /** Env vars that override the agent directory outright. */
  dirEnvs: string[];
}

/** pi sets `AI_AGENT=pi`; omp identifies itself the same way. */
export const HARNESS_DEFAULTS: HarnessDefaults = {
  agentDirs: { pi: ".pi/agent", omp: ".omp/agent" },
  markerEnv: "AI_AGENT",
  dirEnvs: ["PI_CODING_AGENT_DIR", "OMP_CODING_AGENT_DIR"],
};

export interface ResolveAgentDirOptions {
  env?: NodeJS.ProcessEnv;
  /** Module URL of the extension entry point, used to infer an agent dir from an installed location. */
  moduleUrl?: string;
  home?: string;
  exists?: (path: string) => boolean;
  defaults?: HarnessDefaults;
}

export function resolveAgentDir(options: ResolveAgentDirOptions = {}): string {
  const env = options.env ?? process.env;
  const home = options.home ?? homedir();
  const exists = options.exists ?? existsSync;
  const defaults = options.defaults ?? HARNESS_DEFAULTS;

  for (const name of defaults.dirEnvs) {
    const value = env[name];
    if (value?.trim()) return resolve(value.trim().replace(/^~(?=[\\/]|$)/, home));
  }

  const fromLocation = agentDirFromModuleUrl(options.moduleUrl, exists);
  if (fromLocation !== undefined) return fromLocation;

  const marker = env[defaults.markerEnv]?.trim().toLowerCase();
  const markedDir = marker === undefined ? undefined : defaults.agentDirs[marker];
  if (markedDir !== undefined) {
    const candidate = join(home, markedDir);
    if (exists(candidate)) return candidate;
  }

  for (const relative of Object.values(defaults.agentDirs)) {
    const candidate = join(home, relative);
    if (exists(candidate)) return candidate;
  }
  return join(home, defaults.agentDirs.pi);
}

/** `<dir>/agent/extensions/<name>/index.ts` (or `.../extensions/<name>.ts`) -> `<dir>/agent`. */
function agentDirFromModuleUrl(moduleUrl: string | undefined, exists: (path: string) => boolean): string | undefined {
  if (moduleUrl === undefined || moduleUrl === "") return undefined;
  const path = pathFromModuleUrl(moduleUrl);
  if (path === undefined) return undefined;
  let current = dirname(path);
  for (let depth = 0; depth < 4; depth += 1) {
    if (basename(current) === "extensions") {
      const candidate = resolve(dirname(current));
      return exists(candidate) ? candidate : undefined;
    }
    const parent = dirname(current);
    if (parent === current) return undefined;
    current = parent;
  }
  return undefined;
}

/**
 * Parse a `file:` URL without `node:url`, which rejects POSIX-style paths on Windows and vice versa.
 * Keeps the leading slash so POSIX paths stay absolute, and handles `C:/...` and UNC forms.
 */
function pathFromModuleUrl(moduleUrl: string): string | undefined {
  const match = /^file:\/\/(.*)$/i.exec(moduleUrl);
  const rest = match?.[1];
  if (rest === undefined) return undefined;
  let decoded = rest;
  try {
    decoded = decodeURIComponent(rest);
  } catch {
    // Keep the raw text when percent-decoding fails.
  }
  if (decoded.startsWith("/") || /^[A-Za-z]:[\\/]/.test(decoded)) {
    // `file:///C:/...` leaves a leading slash in front of the drive letter.
    return /^\/[A-Za-z]:[\\/]/.test(decoded) ? decoded.slice(1) : decoded;
  }
  return `//${decoded}`;
}

export function detectHarness(options: ResolveAgentDirOptions = {}): HarnessName {
  const marker = (options.env ?? process.env)[(options.defaults ?? HARNESS_DEFAULTS).markerEnv]?.trim().toLowerCase();
  if (marker === "pi" || marker === "omp") return marker;
  return "unknown";
}

/**
 * Authoritative resolution: ask the harness itself when its API is importable. pi exports
 * `getAgentDir()`, and omp is built from the same codebase under its own package name, so whichever
 * harness loaded us answers correctly. Falls back to the heuristic resolution otherwise.
 *
 * The specifier cannot be a static import: it exists only inside a harness (omp rewrites it to its
 * own package through its pi-compat layer), while this module also loads from plain scripts and tests.
 */
export async function resolveAgentDirWithHarness(options: ResolveAgentDirOptions = {}): Promise<string> {
  const override = explicitDir(options);
  if (override !== undefined) return override;
  try {
    const mod: { getAgentDir?: () => string } = await import("@earendil-works/pi-coding-agent");
    const dir = mod.getAgentDir?.();
    if (typeof dir === "string" && dir.trim() !== "") return dir;
  } catch {
    // No harness API available: fall through to the heuristic.
  }
  return resolveAgentDir(options);
}

function explicitDir(options: ResolveAgentDirOptions): string | undefined {
  const env = options.env ?? process.env;
  const home = options.home ?? homedir();
  for (const name of (options.defaults ?? HARNESS_DEFAULTS).dirEnvs) {
    const value = env[name];
    if (value?.trim()) return resolve(value.trim().replace(/^~(?=[\\/]|$)/, home));
  }
  return undefined;
}

function basename(path: string): string {
  const parts = path.split(/[\\/]+/).filter((part) => part !== "");
  return parts.at(-1) ?? "";
}
