import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { ROOT, WORKSPACE } from '../paths.js';

const SECRET_SEGMENTS = new Set(['.ssh']);
const SHELL_META = /(;|&&|\|\||\||>|<|\$\(|`)/;

function splitEnvRoots() {
  return String(process.env.MADARX_ALLOWED_ROOTS || '')
    .split(',')
    .map(item => item.trim())
    .filter(Boolean);
}

function splitEnvReadonlyRoots() {
  return String(process.env.MADARX_READONLY_ROOTS || '')
    .split(',')
    .map(item => item.trim().replace(/^~/, os.homedir()))
    .filter(Boolean);
}

export function getAllowedRoots() {
  return [...new Set([ROOT, WORKSPACE, ...splitEnvRoots(), ...splitEnvReadonlyRoots()].map(item => realpathIfPresent(path.resolve(item))))];
}

/**
 * Returns the set of roots that are READ-ONLY (not writable).
 * Paths under these roots pass resolveSafePath but are blocked by
 * assertWritable / isWritableRoot for any mutating operation.
 */
export function getReadonlyRoots() {
  return [...new Set(splitEnvReadonlyRoots().map(item => realpathIfPresent(path.resolve(item))))];
}

/**
 * Returns true if the resolved absolute path falls exclusively under a
 * readonly root (i.e. is NOT also under a writable root).
 */
export function isReadonlyPath(absPath) {
  const checked = realpathIfPresent(path.resolve(absPath));
  const readonlyRoots = getReadonlyRoots();
  if (readonlyRoots.length === 0) return false;
  const writableRoots = [ROOT, WORKSPACE, ...splitEnvRoots()].map(item => realpathIfPresent(path.resolve(item)));
  const inWritable = writableRoots.some(r => isInside(checked, r));
  if (inWritable) return false;
  return readonlyRoots.some(r => isInside(checked, r));
}

function isInside(child, parent) {
  const rel = path.relative(parent, child);
  return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
}

function hasBlockedSegment(absPath) {
  const parts = absPath.split(path.sep).filter(Boolean);
  if (parts.some(part => SECRET_SEGMENTS.has(part))) return true;
  const base = path.basename(absPath);
  if (base === '.env.local' || /^\.env($|\.)/.test(base)) return true;
  return false;
}

function realpathIfPresent(absPath) {
  if (fs.existsSync(absPath)) return fs.realpathSync(absPath);
  const missing = [];
  let current = absPath;
  while (!fs.existsSync(current)) {
    missing.unshift(path.basename(current));
    const parent = path.dirname(current);
    if (parent === current) return absPath;
    current = parent;
  }
  return path.join(fs.realpathSync(current), ...missing);
}

/**
 * Allowlist-containment check for a directory/file path.
 *
 * Unlike resolveSafePath, this does NOT apply the hardcoded system-directory
 * block (/System, /private, /var, …). It answers exactly one question: is the
 * resolved path contained within one of the OS-allowed roots? This is the
 * correct boundary for a user-designated bulk-import directory tree — the
 * dataset root is added to MADARX_ALLOWED_ROOTS by the operator. Returns false
 * (never throws) so callers can decide how to surface a violation.
 */
export function isInsideAllowedRoots(inputPath, options = {}) {
  const roots = (options.allowedRoots || getAllowedRoots()).map(item => path.resolve(item));
  const checked = realpathIfPresent(path.resolve(String(inputPath || '.')));
  return roots.some(item => isInside(checked, item));
}

export function resolveSafePath(inputPath, options = {}) {
  const roots = (options.allowedRoots || getAllowedRoots()).map(item => path.resolve(item));
  const base = options.base ? path.resolve(options.base) : ROOT;
  const candidate = path.resolve(base, String(inputPath || '.'));
  const checked = realpathIfPresent(candidate);
  const root = roots.find(item => isInside(checked, item));
  if (!root) throw new Error(`Path is outside allowed roots: ${inputPath}`);
  if (hasBlockedSegment(candidate) || hasBlockedSegment(checked)) throw new Error(`Path is blocked by safety policy: ${inputPath}`);
  for (const blocked of ['/System', '/Library', '/private', '/etc', '/var']) {
    if (checked === blocked || checked.startsWith(`${blocked}/`)) {
      throw new Error(`Path is blocked by safety policy: ${inputPath}`);
    }
  }
  return { path: candidate, root };
}

/**
 * Risk tiers for classifyCommand():
 *   'blocked'              — never executes, regardless of trust mode
 *   'irreversible_external'— always requires human approval; trust-mode 'auto' does NOT bypass
 *   'high'                 — requires approval in 'approval_required' mode; bypassed in 'auto'
 *   'medium'               — same gate as high (approval_required gated)
 *   'low'                  — always executes without approval
 */
export function classifyCommand(command, args = [], options = {}) {
  const cmd = String(command || '').trim();
  const argv = Array.isArray(args) ? args.map(String) : [];
  const joined = [cmd, ...argv].join(' ');
  if (!cmd) return { risk: 'blocked', approval_required: true, blocked: true, reason: 'No command provided' };
  if (options.rawString && SHELL_META.test(cmd)) {
    return { risk: 'high', approval_required: true, blocked: false, reason: 'Shell metacharacters require approval' };
  }
  if (cmd === 'sudo' || joined.includes('rm -rf') || (cmd === 'git' && argv[0] === 'reset' && argv.includes('--hard'))) {
    return { risk: 'blocked', approval_required: true, blocked: true, reason: 'Destructive command is blocked' };
  }
  // Irreversible external: git force-push — cannot be undone on the remote.
  if (cmd === 'git' && argv[0] === 'push' && (argv.includes('--force') || argv.includes('-f'))) {
    return { risk: 'irreversible_external', approval_required: true, blocked: false, reason: 'Force-push rewrites remote history and cannot be undone' };
  }
  // Irreversible external: kubectl delete — removes live cluster resources.
  if (cmd === 'kubectl' && argv[0] === 'delete') {
    return { risk: 'irreversible_external', approval_required: true, blocked: false, reason: 'kubectl delete removes live cluster resources and cannot be undone' };
  }
  // Irreversible external: destructive SQL via SQL client CLIs.
  // Conservative regex — false positives are better than false negatives.
  if (['psql', 'mysql', 'sqlite3', 'pgcli', 'mycli'].includes(cmd)) {
    const sqlPattern = /\b(DROP\s+TABLE|DROP\s+DATABASE|TRUNCATE|DELETE\s+FROM\s+\w+\s*(?:;|$))/i;
    if (sqlPattern.test(joined)) {
      return { risk: 'irreversible_external', approval_required: true, blocked: false, reason: 'Destructive SQL (DROP/TRUNCATE/DELETE without WHERE) cannot be undone' };
    }
  }
  if ((cmd === 'git' && ['commit', 'push'].includes(argv[0])) || ['chmod', 'chown', 'kill', 'pkill'].includes(cmd) || joined.includes('deploy')) {
    return { risk: 'high', approval_required: true, blocked: false, reason: 'Command can modify history, permissions, processes, or deployments' };
  }
  if (['npm', 'pnpm', 'yarn', 'bun'].includes(cmd) && ['install', 'add', 'remove'].includes(argv[0])) {
    return { risk: 'medium', approval_required: true, blocked: false, reason: 'Package manager mutation requires approval' };
  }
  if (['curl', 'wget', 'python', 'python3', 'node'].includes(cmd) && !(cmd === 'node' && argv[0] === '--version')) {
    return { risk: 'medium', approval_required: true, blocked: false, reason: 'General command execution requires approval' };
  }
  const low = [
    ['pwd'], ['date'], ['node', '--version'], ['npm', 'test'], ['npm', 'run', 'check'],
    ['git', 'status'], ['git', 'diff'], ['git', 'log'],
  ];
  if (low.some(pattern => pattern.every((part, idx) => [cmd, ...argv][idx] === part))) {
    return { risk: 'low', approval_required: false, blocked: false, reason: 'Read-only or verification command' };
  }
  return { risk: 'medium', approval_required: true, blocked: false, reason: 'Command is not on the low-risk allowlist' };
}

export function requiresApproval(action) {
  if (!action) return false;
  if (typeof action === 'string') return ['write', 'patch', 'terminal_high', 'process_kill'].includes(action);
  return Boolean(action.approval_required || ['medium', 'high', 'irreversible_external'].includes(action.risk));
}

export function redactToolOutput(text) {
  return String(text || '')
    .replace(/\b(sk|pk|xox[baprs]|ghp|github_pat|AIza)[A-Za-z0-9_\-]{12,}\b/g, '[REDACTED_TOKEN]')
    .replace(/\b[A-Za-z0-9._%+-]+_API_KEY\s*=\s*[^\s]+/gi, '[REDACTED_API_KEY]')
    .replace(/\b(api[_-]?key|token|secret|password)\s*[:=]\s*['"]?[^'"\s]+/gi, '$1=[REDACTED]');
}
