'use strict';

const fs = require('fs');
const path = require('path');
const { parseAgentWorktreePath } = require('./resume-cwd');

function normalizePath(value) {
  const raw = String(value || '').trim();
  if (!raw) return '';
  const expanded = raw.replace(/^~(?=\/|$)/, process.env.HOME || '~');
  return path.normalize(path.isAbsolute(expanded) ? expanded : path.resolve(expanded));
}

// realpathSync(dir) does an lstat() per path component, and on a deep iCloud/Dropbox
// CloudStorage path each lstat can be a slow userspace round-trip. resolveSessionWorkspaceBinding
// calls safeRealpath multiplicatively — findWorktreeByPath realpaths EVERY worktree and
// re-realpaths the same candidatePath once per worktree, per candidate, per session — so a
// single session-list broadcast (≈17 sessions × ≈36 worktrees) fired tens of thousands of
// lstat syscalls on the main thread (the dominant non-idle frame in the CPU profile: `lstat`
// 95%-attributed to safeRealpath). Worktree/cwd canonical paths are stable for a process, so
// memoize with a short TTL: a cache hit collapses the storm to ~one realpathSync per distinct
// path per TTL window, while the TTL still lets a path that is created/moved re-resolve.
const _REALPATH_TTL_MS = Number(process.env.CTM_REALPATH_CACHE_TTL_MS) || 60000;
const _REALPATH_CACHE_MAX = 4000;
const _realpathCache = new Map(); // normalized input -> { real, at }

function safeRealpath(value) {
  const normalized = normalizePath(value);
  if (!normalized) return '';
  const now = Date.now();
  const hit = _realpathCache.get(normalized);
  if (hit && (now - hit.at) < _REALPATH_TTL_MS) return hit.real;
  let real;
  try { real = fs.realpathSync(normalized); }
  catch { real = normalized; }
  // Bound the cache (FIFO eviction) so a long-lived process with churning paths can't grow it.
  if (_realpathCache.size >= _REALPATH_CACHE_MAX) {
    const oldest = _realpathCache.keys().next().value;
    if (oldest !== undefined) _realpathCache.delete(oldest);
  }
  _realpathCache.set(normalized, { real, at: now });
  return real;
}

// Test/maintenance hook: drop all memoized realpaths (e.g. after a worktree move in a test).
function _clearRealpathCache() { _realpathCache.clear(); }

function normalizeBranch(value) {
  return String(value || '').trim().replace(/^refs\/heads\//, '');
}

function isMainBranch(value) {
  const branch = normalizeBranch(value);
  return branch === 'main' || branch === 'master';
}

function normalizeWorktree(worktree) {
  if (!worktree) return null;
  const wtPath = normalizePath(worktree.path || worktree.worktreePath || '');
  const branch = normalizeBranch(worktree.branch || '');
  if (!wtPath && !branch) return null;
  return {
    ...worktree,
    path: wtPath,
    realPath: safeRealpath(wtPath),
    branch,
    worktreeName: worktree.worktreeName || path.basename(wtPath || branch),
    isMain: worktree.isMain === true || isMainBranch(branch) || worktree.state === 'primary',
  };
}

function worktreePathMatches(wt, candidatePath) {
  if (!wt || !candidatePath) return false;
  const candidate = safeRealpath(candidatePath);
  const wtPath = wt.realPath || safeRealpath(wt.path);
  return !!(candidate && wtPath && (candidate === wtPath || candidate.startsWith(wtPath + path.sep)));
}

function findWorktreeByPath(worktrees, candidatePath) {
  const matches = (worktrees || [])
    .map(normalizeWorktree)
    .filter(Boolean)
    .filter(wt => worktreePathMatches(wt, candidatePath))
    .sort((a, b) => String(b.path || '').length - String(a.path || '').length);
  return matches[0] || null;
}

function findWorktreesByBranch(worktrees, branch) {
  const clean = normalizeBranch(branch);
  if (!clean) return [];
  return (worktrees || [])
    .map(normalizeWorktree)
    .filter(Boolean)
    .filter(wt => wt.branch === clean);
}

function pushCandidate(candidates, source, candidatePath, branch, priority, extra = {}) {
  const normalized = normalizePath(candidatePath);
  if (!normalized) return;
  candidates.push({
    source,
    path: normalized,
    branch: normalizeBranch(branch),
    priority,
    ...extra,
  });
}

function runtimeContextFromSession(session) {
  const raw = session?._runtimeWorkContext || session?.runtimeWorkContext || session?.runtime_work_context || null;
  if (!raw || typeof raw !== 'object') return null;
  const cwd = normalizePath(raw.cwd || raw.currentDir || raw.current_dir || raw.path || '');
  const worktreePath = normalizePath(raw.worktreePath || raw.worktree_path || raw.gitWorktree || raw.git_worktree || '');
  if (!cwd && !worktreePath) return null;
  return {
    cwd,
    worktreePath,
    branch: normalizeBranch(raw.branch || raw.gitBranch || raw.git_branch || ''),
    source: String(raw.source || 'runtime.current').trim() || 'runtime.current',
    confidence: String(raw.confidence || 'current').trim() || 'current',
    observedAt: Number(raw.observedAt || raw.observed_at || raw.timestamp || 0) || 0,
  };
}

function identityCandidates(identity) {
  const out = [];
  if (!identity || typeof identity !== 'object') return out;
  const ctm = identity.ctm || null;
  if (ctm) {
    pushCandidate(out, 'ctm.project_path', ctm.project_path, ctm.git_branch || '', 30);
    pushCandidate(out, 'ctm.cwd', ctm.cwd, ctm.git_branch || '', 35);
  }
  const agents = Array.isArray(identity.agents) ? identity.agents : [];
  agents.forEach((agent, idx) => {
    pushCandidate(out, `agent_sessions[${idx}].project_path`, agent?.project_path, agent?.git_branch, 40 + idx);
  });
  return out;
}

function makeBinding(fields) {
  const branch = normalizeBranch(fields.branch || '');
  const worktreePath = normalizePath(fields.worktreePath || '');
  const runtimeBranch = normalizeBranch(fields.runtimeBranch || '');
  const isMain = fields.isMain != null ? !!fields.isMain : isMainBranch(branch);
  const binding = {
    status: fields.status || 'unknown',
    branch,
    displayBranch: branch && !isMain ? branch : '',
    worktreePath,
    worktreeName: fields.worktreeName || path.basename(worktreePath || branch || ''),
    isMain,
    source: fields.source || '',
    confidence: fields.confidence || 'none',
    runtimeBranch,
    runtimeCwd: normalizePath(fields.runtimeCwd || ''),
    runtimeWorktreePath: normalizePath(fields.runtimeWorktreePath || ''),
    currentRuntime: !!fields.currentRuntime,
    runtimeContextSource: fields.runtimeContextSource || '',
    runtimeContextObservedAt: Number(fields.runtimeContextObservedAt || 0) || 0,
    hidesRuntimeBranch: !!fields.hidesRuntimeBranch,
    mismatchReason: fields.mismatchReason || '',
    candidates: Array.isArray(fields.candidates) ? fields.candidates : [],
  };
  return binding;
}

function candidateKey(candidate) {
  const parsed = parseAgentWorktreePath(candidate.path);
  return parsed ? parsed.cwd : safeRealpath(candidate.path);
}

function resolveSessionWorkspaceBinding(session, options = {}) {
  const worktrees = Array.isArray(options.worktrees) ? options.worktrees : [];
  const runtimeCwd = normalizePath(session?.cwd || '');
  const runtimeWorktreePath = normalizePath(session?.worktree_path || '');
  const runtimeBranch = normalizeBranch(session?.branch || '');
  const currentRuntime = runtimeContextFromSession(session);
  const candidates = [];

  if (currentRuntime) {
    pushCandidate(candidates, 'runtime.current_worktree_path', currentRuntime.worktreePath, currentRuntime.branch || runtimeBranch, -20, {
      currentRuntime: true,
      runtimeContextSource: currentRuntime.source,
      runtimeContextConfidence: currentRuntime.confidence,
      runtimeContextObservedAt: currentRuntime.observedAt,
    });
    pushCandidate(candidates, 'runtime.current_cwd', currentRuntime.cwd, currentRuntime.branch || runtimeBranch, -10, {
      currentRuntime: true,
      runtimeContextSource: currentRuntime.source,
      runtimeContextConfidence: currentRuntime.confidence,
      runtimeContextObservedAt: currentRuntime.observedAt,
    });
  }
  pushCandidate(candidates, 'runtime.worktree_path', runtimeWorktreePath, runtimeBranch, 0);
  pushCandidate(candidates, 'runtime.cwd', runtimeCwd, runtimeBranch, 10);
  for (const candidate of identityCandidates(options.identity)) candidates.push(candidate);

  const verified = [];
  for (const candidate of candidates) {
    const wt = findWorktreeByPath(worktrees, candidate.path);
    const parsed = parseAgentWorktreePath(candidate.path);
    if (!wt && !parsed) continue;
    verified.push({
      ...candidate,
      key: wt ? (wt.realPath || safeRealpath(wt.path)) : candidateKey(candidate),
      worktree: wt,
      parsed,
      branch: normalizeBranch((wt && wt.branch) || (candidate.currentRuntime && parsed && parsed.worktreeName) || candidate.branch || (parsed && parsed.worktreeName) || ''),
      priority: candidate.priority,
    });
  }

  verified.sort((a, b) => a.priority - b.priority);
  const currentVerified = verified.find(item => item.currentRuntime);
  if (currentVerified) {
    const wt = currentVerified.worktree;
    const worktreePath = normalizePath((wt && wt.path) || currentVerified.path);
    const branch = normalizeBranch(currentVerified.branch || (wt && wt.branch) || '');
    const isMain = wt ? wt.isMain : isMainBranch(branch);
    return makeBinding({
      status: 'bound',
      branch,
      worktreePath,
      worktreeName: (wt && wt.worktreeName) || (currentVerified.parsed && currentVerified.parsed.worktreeName) || path.basename(worktreePath),
      isMain,
      source: currentVerified.source,
      confidence: wt ? 'verified-current' : 'path-pattern-current',
      runtimeBranch,
      runtimeCwd,
      runtimeWorktreePath,
      currentRuntime: true,
      runtimeContextSource: currentVerified.runtimeContextSource || '',
      runtimeContextObservedAt: currentVerified.runtimeContextObservedAt || 0,
      hidesRuntimeBranch: !!(runtimeBranch && branch && runtimeBranch !== branch),
      mismatchReason: runtimeBranch && branch && runtimeBranch !== branch ? 'runtime_branch_replaced_by_current_context' : '',
      candidates: verified.map(item => ({ source: item.source, branch: item.branch, path: item.path, currentRuntime: !!item.currentRuntime })),
    });
  }

  const nonMainVerified = verified.filter(item => item.branch && !isMainBranch(item.branch));
  if (nonMainVerified.length > 0) {
    const keys = new Set(nonMainVerified.map(item => item.key).filter(Boolean));
    const runtimeVerified = nonMainVerified.find(item => item.source === 'runtime.worktree_path' || item.source === 'runtime.cwd');
    const selected = runtimeVerified || (keys.size === 1 ? nonMainVerified[0] : null);
    if (selected) {
      const wt = selected.worktree;
      const worktreePath = normalizePath((wt && wt.path) || selected.path);
      return makeBinding({
        status: 'bound',
        branch: selected.branch,
        worktreePath,
        worktreeName: (wt && wt.worktreeName) || (selected.parsed && selected.parsed.worktreeName) || path.basename(worktreePath),
        isMain: false,
        source: selected.source,
        confidence: wt ? 'verified' : 'path-pattern',
        runtimeBranch,
        runtimeCwd,
        runtimeWorktreePath,
        candidates: nonMainVerified.map(item => ({ source: item.source, branch: item.branch, path: item.path })),
      });
    }
    return makeBinding({
      status: 'ambiguous',
      branch: '',
      source: 'conflict',
      confidence: 'none',
      runtimeBranch,
      runtimeCwd,
      runtimeWorktreePath,
      hidesRuntimeBranch: !!runtimeBranch && !isMainBranch(runtimeBranch),
      mismatchReason: 'multiple_verified_worktree_candidates',
      candidates: nonMainVerified.map(item => ({ source: item.source, branch: item.branch, path: item.path })),
    });
  }

  const mainPath = runtimeWorktreePath || runtimeCwd;
  const mainWt = findWorktreeByPath(worktrees, mainPath);
  if (mainWt) {
    return makeBinding({
      status: 'bound',
      branch: mainWt.branch || runtimeBranch,
      worktreePath: mainWt.path,
      worktreeName: mainWt.worktreeName || path.basename(mainWt.path || ''),
      isMain: mainWt.isMain,
      source: mainWt.isMain ? 'runtime.main_worktree' : 'runtime.worktree',
      confidence: 'verified',
      runtimeBranch,
      runtimeCwd,
      runtimeWorktreePath,
      hidesRuntimeBranch: runtimeBranch && !isMainBranch(runtimeBranch) && mainWt.isMain,
      mismatchReason: runtimeBranch && !isMainBranch(runtimeBranch) && mainWt.isMain ? 'runtime_branch_not_verified_for_cwd' : '',
    });
  }

  if (runtimeBranch && !isMainBranch(runtimeBranch)) {
    const branchMatches = findWorktreesByBranch(worktrees, runtimeBranch);
    if (branchMatches.length === 1 && runtimeWorktreePath) {
      const wt = branchMatches[0];
      return makeBinding({
        status: 'bound',
        branch: wt.branch,
        worktreePath: wt.path,
        worktreeName: wt.worktreeName || path.basename(wt.path || ''),
        isMain: wt.isMain,
        source: 'runtime.branch_verified_by_worktree_path',
        confidence: 'verified',
        runtimeBranch,
        runtimeCwd,
        runtimeWorktreePath,
      });
    }
    return makeBinding({
      status: 'unverified',
      branch: '',
      source: 'runtime.branch',
      confidence: 'none',
      runtimeBranch,
      runtimeCwd,
      runtimeWorktreePath,
      hidesRuntimeBranch: true,
      mismatchReason: 'runtime_branch_without_verified_worktree',
    });
  }

  return makeBinding({
    status: runtimeBranch ? 'main' : 'unknown',
    branch: runtimeBranch,
    isMain: isMainBranch(runtimeBranch),
    source: runtimeBranch ? 'runtime.branch' : '',
    confidence: runtimeBranch ? 'runtime' : 'none',
    runtimeBranch,
    runtimeCwd,
    runtimeWorktreePath,
  });
}

function sessionForWorkspaceBinding(session, binding) {
  if (!session || !binding || !binding.worktreePath) return session;
  return {
    ...session,
    branch: binding.branch || session.branch,
    worktree_path: binding.worktreePath || session.worktree_path,
    cwd: binding.worktreePath || session.cwd,
  };
}

module.exports = {
  findWorktreeByPath,
  findWorktreesByBranch,
  isMainBranch,
  normalizeBranch,
  normalizePath,
  resolveSessionWorkspaceBinding,
  sessionForWorkspaceBinding,
  _clearRealpathCache,
};
