import { existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { isAbsolute, join, resolve as resolvePath } from 'path';
import { RESOLVER_FILENAMES, hasResolverFile } from './resolver-filenames.ts';
import { isPathContained } from './path-confine.ts';

/**
 * Walk up from `startDir` looking for a `skills/` directory that
 * contains a recognized resolver file (`RESOLVER.md` or `AGENTS.md`).
 * Returns the absolute directory containing `skills/` or null if no
 * such directory is found within 10 levels.
 *
 * `startDir` is parameterized so tests can run hermetically against
 * fixtures. Default matches the prior `doctor.ts`-private implementation.
 */
export function findRepoRoot(startDir: string = process.cwd()): string | null {
  let dir = startDir;
  for (let i = 0; i < 10; i++) {
    if (hasResolverFile(join(dir, 'skills'))) return dir;
    const parent = join(dir, '..');
    if (parent === dir) break;
    dir = parent;
  }
  return null;
}

/**
 * Where auto-detect found the skills directory.
 *   - `env_explicit`                 — $GBRAIN_SKILLS_DIR (operator override; v0.31.7)
 *   - `openclaw_workspace_env`       — $OPENCLAW_WORKSPACE/skills
 *   - `openclaw_workspace_env_root`  — $OPENCLAW_WORKSPACE/ (AGENTS.md at
 *                                      workspace root; skills in subdir)
 *   - `openclaw_workspace_home`      — ~/.openclaw/workspace/skills
 *   - `openclaw_workspace_home_root` — ~/.openclaw/workspace (root AGENTS.md)
 *   - `repo_root`                    — walked up from cwd, found gbrain repo
 *   - `cwd_skills`                   — ./skills fallback
 *   - `install_path`                 — walked up from this module's install
 *                                      path; READ-ONLY callers only (v0.31.7)
 */
export type SkillsDirSource =
  | 'env_explicit'
  | 'openclaw_workspace_env'
  | 'openclaw_workspace_env_root'
  | 'openclaw_workspace_home'
  | 'openclaw_workspace_home_root'
  | 'cwd_walk_up'
  | 'repo_root'
  | 'cwd_skills'
  | 'install_path';

export interface SkillsDirDetection {
  dir: string | null;
  source: SkillsDirSource | null;
}

/**
 * Given a workspace root, resolve where the skills directory should
 * live. Returns the skills dir + the specific source variant. Returns
 * null if neither `workspace/skills/<RESOLVER|AGENTS>` nor
 * `workspace/<AGENTS|RESOLVER>` exists.
 *
 * `sourceSubdir` / `sourceRoot` let callers distinguish "skills-dir
 * variant" from "workspace-root variant" for --verbose logging.
 */
function resolveWorkspaceSkillsDir(
  workspace: string,
  sourceSubdir: SkillsDirSource,
  sourceRoot: SkillsDirSource,
): SkillsDirDetection | null {
  const subdir = join(workspace, 'skills');
  // Refuse a `skills/` that escapes the declared workspace via symlink (#419).
  // isPathContained realpaths both ends, so `workspace/skills` → /etc is
  // rejected, while a legit in-workspace symlink (`workspace/skills` →
  // `workspace/_real-skills`) stays contained and is allowed. A non-contained
  // candidate returns null so lower tiers can try, rather than trusting an escape.
  const contained = isPathContained(subdir, workspace);
  // Preferred: workspace/skills with a resolver file inside it (gbrain-native).
  if (hasResolverFile(subdir)) {
    return contained ? { dir: subdir, source: sourceSubdir } : null;
  }
  // Fallback: resolver file at workspace root (OpenClaw-native layout).
  // The skills/ subtree still governs file layout even when routing lives
  // at workspace root. Return the skills subdir so downstream file lookups
  // work; the resolver parser knows how to look one level up.
  if (hasResolverFile(workspace) && existsSync(subdir)) {
    return contained ? { dir: subdir, source: sourceRoot } : null;
  }
  return null;
}

/**
 * Auto-detect the skills directory. Priority (v0.31.7 read+write-safe order):
 *   0. $GBRAIN_SKILLS_DIR explicit operator override (any caller)
 *   1. $OPENCLAW_WORKSPACE when explicitly set (env > repo-root walk)
 *   2. ~/.openclaw/workspace/ (user's default OpenClaw deployment)
 *   3. findRepoRoot() walk from cwd (gbrain's own repo)
 *   4. ./skills fallback (dev scratch, fixtures)
 *
 * Tier 0 ($GBRAIN_SKILLS_DIR) is safe for both read and write paths because
 * the operator explicitly set the variable — opt-in retargeting is fine. The
 * silent retargeting risk that motivates `autoDetectSkillsDirReadOnly` is
 * about implicit fallback to install-path when no explicit signal is set.
 *
 * The prior order put `findRepoRoot` first, which meant
 * `export OPENCLAW_WORKSPACE=...; gbrain check-resolvable` run from
 * inside the gbrain repo silently shadowed the env var by walking up
 * to gbrain's own skills/. Explicit env should win. Unset env → behavior
 * is unchanged from before.
 *
 * Write-path callers (skillpack install, skillify scaffold,
 * post-install-advisory) MUST use this function, not the read-only variant —
 * a write-path install-path fallback would let `gbrain skillpack install`
 * from `~` silently target the bundled gbrain repo's skills/ instead of the
 * user's workspace.
 *
 * `startDir` + `env` params keep tests hermetic.
 */
export function autoDetectSkillsDir(
  startDir: string = process.cwd(),
  env: NodeJS.ProcessEnv = process.env,
): SkillsDirDetection {
  // 0. $GBRAIN_SKILLS_DIR explicit operator override. Safe for all callers
  //    because the operator explicitly set the env var. Does NOT support the
  //    `workspace-root with AGENTS.md + skills/ sibling` shape — operator who
  //    wants that should point the env var at the skills/ dir directly.
  if (env.GBRAIN_SKILLS_DIR) {
    const explicit = isAbsolute(env.GBRAIN_SKILLS_DIR)
      ? env.GBRAIN_SKILLS_DIR
      : resolvePath(startDir, env.GBRAIN_SKILLS_DIR);
    if (hasResolverFile(explicit)) {
      return { dir: explicit, source: 'env_explicit' };
    }
    // Fall through — invalid env override doesn't crash, lets lower tiers try.
  }

  // 1. $OPENCLAW_WORKSPACE wins when explicitly set.
  if (env.OPENCLAW_WORKSPACE) {
    const workspace = isAbsolute(env.OPENCLAW_WORKSPACE)
      ? env.OPENCLAW_WORKSPACE
      : resolvePath(startDir, env.OPENCLAW_WORKSPACE);
    const resolved = resolveWorkspaceSkillsDir(
      workspace,
      'openclaw_workspace_env',
      'openclaw_workspace_env_root',
    );
    if (resolved) return resolved;
  }

  // 1b. (v0.33) Walk up from cwd looking for any `skills/` dir. No
  //     resolver-file gating — this is for non-OpenClaw hosts (any
  //     agent repo with a bare `skills/` directory, before a resolver
  //     file is written). Stops at the first ancestor with a `skills/`
  //     subdirectory. Comes after $OPENCLAW_WORKSPACE so R5
  //     (precedence regression) holds: explicit env still wins. Comes
  //     before ~/.openclaw/workspace so that `cd ~/git/your-agent-repo
  //     && gbrain skillpack scaffold X` finds the agent repo, not an
  //     implicit fallback to OpenClaw's default install.
  {
    let dir = startDir;
    for (let i = 0; i < 10; i++) {
      const candidate = join(dir, 'skills');
      // Only accept a `skills/` contained within the ancestor it was found under
      // (#419). An escaping symlink is skipped, and the walk continues upward
      // rather than trusting a dir that resolves outside the boundary.
      if (existsSync(candidate) && isPathContained(candidate, dir)) {
        return { dir: candidate, source: 'cwd_walk_up' };
      }
      const parent = join(dir, '..');
      const resolvedParent = resolvePath(parent);
      const resolvedDir = resolvePath(dir);
      if (resolvedParent === resolvedDir) break;
      dir = resolvedParent;
    }
  }

  // 2. ~/.openclaw/workspace as the default user-level OpenClaw deployment.
  if (env.HOME) {
    const workspace = join(env.HOME, '.openclaw', 'workspace');
    const resolved = resolveWorkspaceSkillsDir(
      workspace,
      'openclaw_workspace_home',
      'openclaw_workspace_home_root',
    );
    if (resolved) return resolved;
  }

  // 3. gbrain repo walk from cwd.
  const repoRoot = findRepoRoot(startDir);
  if (repoRoot && isGbrainRepoRoot(repoRoot)) {
    const skillsDir = join(repoRoot, 'skills');
    if (isPathContained(skillsDir, repoRoot)) {
      return { dir: skillsDir, source: 'repo_root' };
    }
  }

  // 4. ./skills fallback (with hasResolverFile gate). Functionally
  // subsumed by tier 1b's `cwd_walk_up` (broader, no resolver gate),
  // but kept for callers that explicitly want to distinguish a
  // resolver-bearing fallback from a plain skills-dir match.
  // In practice this tier never fires after 1b — cwd_walk_up matches
  // the same path first. Kept in the type union for back-compat.
  const cwdSkills = join(startDir, 'skills');
  if (hasResolverFile(cwdSkills) && isPathContained(cwdSkills, startDir)) {
    return { dir: cwdSkills, source: 'cwd_skills' };
  }

  return { dir: null, source: null };
}

function isGbrainRepoRoot(dir: string): boolean {
  return (
    existsSync(join(dir, 'src', 'cli.ts')) &&
    hasResolverFile(join(dir, 'skills'))
  );
}

/**
 * Read-only skills-dir detection (v0.31.7). Wraps `autoDetectSkillsDir` and
 * adds an install-path fallback when the primary detection returns null —
 * walks up from this module's install location to find a gbrain repo root,
 * gated by `isGbrainRepoRoot` to avoid false-positive on unrelated repos.
 *
 * Use this from READ-ONLY callers only: `gbrain doctor`,
 * `gbrain check-resolvable`, `gbrain routing-eval`. Never from write paths.
 *
 * Why a separate function? `autoDetectSkillsDir` is shared with write paths
 * (`skillpack install`, `skillify scaffold`, `post-install-advisory`).
 * Adding the install-path fallback to the shared function would let
 * `gbrain skillpack install` from `~` silently target the bundled gbrain
 * repo's skills/ instead of the user's actual workspace — a quiet data-flow
 * regression. Read-only callers don't write anything to the resolved path,
 * so the install-path fallback is safe for them.
 *
 * Closes the install-path footgun for hosted-CLI installs (`bun install -g
 * github:garrytan/gbrain && cd ~ && gbrain doctor`) without expanding the
 * blast radius to write-path callers.
 */
export function autoDetectSkillsDirReadOnly(
  startDir: string = process.cwd(),
  env: NodeJS.ProcessEnv = process.env,
): SkillsDirDetection {
  const primary = autoDetectSkillsDir(startDir, env);
  if (primary.dir) return primary;

  // Tier-5 install-path fallback: walk up from this module's install
  // location. Gate with isGbrainRepoRoot so we don't false-positive when
  // the install path lives inside an unrelated repo (e.g., a monorepo
  // that vendored gbrain in a subdir).
  try {
    const moduleDir = fileURLToPath(import.meta.url);
    const installRoot = findRepoRoot(moduleDir);
    if (installRoot && isGbrainRepoRoot(installRoot)) {
      const skillsDir = join(installRoot, 'skills');
      if (isPathContained(skillsDir, installRoot)) {
        return { dir: skillsDir, source: 'install_path' };
      }
    }
  } catch {
    // fileURLToPath can throw on malformed import.meta.url (rare; some
    // bundlers/runtimes). Fall through to the null detection — better to
    // refuse the fallback than to fabricate a path.
  }

  return primary; // null detection, source: null
}

/**
 * Human-readable summary of the resolver-file search paths, for error
 * messages when auto-detect fails. Mirrors the priority order used by
 * `autoDetectSkillsDir`.
 */
export const AUTO_DETECT_HINT = [
  `  1. --skills-dir flag`,
  `  2. $GBRAIN_SKILLS_DIR (explicit operator override)`,
  `  3. $OPENCLAW_WORKSPACE/{skills/,}{${RESOLVER_FILENAMES.join(',')}}`,
  `  4. cwd + walk-up for any skills/ directory (v0.33; for non-OpenClaw hosts)`,
  `  5. ~/.openclaw/workspace/{skills/,}{${RESOLVER_FILENAMES.join(',')}}`,
  `  6. repo root with skills/${RESOLVER_FILENAMES.join(' or skills/')}`,
  `  7. ./skills/${RESOLVER_FILENAMES.join(' or ./skills/')}`,
].join('\n');

/**
 * Read-only auto-detect hint. Includes the install-path fallback that
 * `autoDetectSkillsDirReadOnly` adds for `gbrain doctor` /
 * `gbrain check-resolvable` / `gbrain routing-eval`.
 */
export const AUTO_DETECT_HINT_READ_ONLY = [
  AUTO_DETECT_HINT,
  `  7. (read-only) walk up from gbrain's install path`,
].join('\n');
