/**
 * SSH command policy: built-in deny patterns + per-connection custom regex.
 *
 * Layers (evaluated in order against the candidate command line):
 *   1. BUILT-IN deny      — destructive operations we never want from automation
 *   2. CUSTOM deny        — per-connection admin/operator additions
 *   3. CUSTOM allow       — when non-empty, command must match at least one
 *                           (otherwise denied as `not_in_allowlist`)
 *
 * Custom regex sources are validated at save time with three caps:
 *   - max 16 patterns per connection
 *   - max 256 chars per pattern
 *   - structural ReDoS check (nested quantifier rejected; no external dep)
 *
 * Invalid custom regex → save error (caller surfaces to UI); never silently
 * dropped, so the operator is never confused about what's enforced.
 *
 * Plan: docs/superpowers/plans/2026-05-12-ssh-tool-integration.md (Phase 2).
 */

export interface DenyPattern {
  /** Stable key for logs / audit / UI. */
  name: string;
  regex: RegExp;
  description: string;
}

/**
 * Destructive shell patterns we never permit. These are intentionally narrow
 * — false-positives are worse than false-negatives at this layer (the LLM
 * will give up early and the user will have to debug). Operators are
 * expected to add their own deny patterns for site-specific concerns.
 *
 * All patterns are case-INSENSITIVE and tested against the raw command line.
 */
export const BUILTIN_DENY_PATTERNS: readonly DenyPattern[] = [
  {
    name: 'rm_rf_system_dir',
    regex: /\brm\s+(?:-[A-Za-z]*[rf][A-Za-z]*\s+)+(?:--no-preserve-root\s+)?\/(?:etc|usr|var|boot|bin|sbin|lib|lib64|opt|root|sys|proc)(?:\s|\/|$|;|\||&)/i,
    description: 'rm -rf on a system directory',
  },
  {
    name: 'rm_rf_root',
    regex: /\brm\s+(?:-[A-Za-z]*[rf][A-Za-z]*\s+)+(?:--no-preserve-root\s+)?\/(?![A-Za-z0-9_.-])/i,
    description: 'rm -rf / or rm -rf /*',
  },
  {
    name: 'dd_to_block_device',
    regex: /\bdd\b[^\n]*\bof=\/dev\/(?:sd[a-z]|hd[a-z]|nvme\d|vd[a-z]|xvd[a-z]|mmcblk\d|loop\d)/i,
    description: 'dd writing to a block device',
  },
  {
    name: 'mkfs',
    regex: /\bmkfs(?:\.[A-Za-z0-9]+)?\b/i,
    description: 'mkfs (format filesystem)',
  },
  {
    name: 'fork_bomb',
    regex: /:\(\)\s*\{[^}]*:\s*\|[^}]*:[^}]*\}\s*;?\s*:/,
    description: 'classic :(){ :|:& };: fork bomb',
  },
  {
    name: 'shutdown_or_reboot',
    regex: /\b(?:shutdown|reboot|poweroff|halt|init\s+0|init\s+6)\b/i,
    description: 'system power state change',
  },
  {
    name: 'kill_init',
    regex: /\bkill\s+(?:-(?:9|KILL|SIGKILL|TERM|SIGTERM)\s+)?1\b/i,
    description: 'kill PID 1 (init)',
  },
  {
    name: 'pipe_curl_to_shell',
    regex: /\b(?:curl|wget|fetch)\b[^|\n]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh|ksh|dash|fish)\b/i,
    description: 'curl/wget piped to shell',
  },
  {
    name: 'reverse_shell_tcp',
    regex: /(?:bash|sh|zsh|ksh)\s+-i\s+>&?\s*\/dev\/tcp\//i,
    description: 'bash -i >& /dev/tcp/ reverse shell',
  },
  {
    name: 'nc_exec_shell',
    regex: /\bn(?:c|cat)\b[^\n]*-e\s+\/(?:bin\/)?(?:bash|sh|zsh)/i,
    description: 'netcat -e /bin/sh reverse shell',
  },
  {
    name: 'overwrite_etc_passwd',
    regex: /(?:^|[\s;|&])>{1,2}\s*\/etc\/(?:passwd|shadow|sudoers|gshadow)\b/i,
    description: 'redirect over /etc/passwd-class files',
  },
  {
    name: 'chmod_777_root',
    regex: /\bchmod\s+-R\s+0?777\s+\/(?:\s|$)/i,
    description: 'chmod -R 777 /',
  },
  {
    name: 'history_clear',
    regex: /\bhistory\s+-c\b|(?:^|[\s;|&])>{1,2}\s*~?\/?\.?(?:bash_|zsh_)?history\b|\b(?:unset|export)\s+HISTFILE\b/i,
    description: 'clear or disable shell history',
  },
];

export interface ValidateCustomResult {
  ok: boolean;
  /** Compiled regexes when ok=true (1:1 with input order). */
  compiled?: RegExp[];
  /** Per-index errors when ok=false. */
  errors?: Array<{ index: number; reason: ValidateRejection }>;
}

export type ValidateRejection =
  | 'too_many'
  | 'too_long'
  | 'empty'
  | 'invalid_regex'
  | 'nested_quantifier'
  | 'forbidden_construct';

export const MAX_CUSTOM_PATTERNS = 16;
export const MAX_PATTERN_LENGTH = 256;

/**
 * Heuristic ReDoS check: rejects patterns with a quantifier applied to a
 * group whose own contents include a quantifier ("star height > 1"). This
 * catches the most common catastrophic-backtracking shape, like `(\w+)+`
 * or `(.*)*`.
 *
 * Imperfect — it does not detect overlapping alternation like `(a|aa)+`.
 * Operators with deep regex needs should run their own tests.
 */
function hasNestedQuantifier(source: string): boolean {
  // Match: '(' + optional non-capture marker + body + ')' + quantifier,
  // where body contains a quantifier (+ * ? or { ).
  // Limit to non-nested groups (no inner parens) so we don't blow up on
  // arbitrarily complex source.
  const re = /\((?:\?[:=!<])?[^()]*[+*?][^()]*\)[+*?{]/;
  if (re.test(source)) return true;
  // Also flag (?:.*)+ and (?:.+)+ where . isn't an additional quantifier
  // catch — already covered by the above. Good enough.
  return false;
}

function hasForbiddenConstruct(source: string): boolean {
  // Refuse Unicode-property escapes (\p{...}) and named groups — they're
  // legitimate, but rare enough that operators are unlikely to need them
  // here. Forbidding them keeps the surface area minimal. Comment them in
  // if a future use case appears.
  if (/\\p\{/.test(source)) return true;
  if (/\(\?P?</.test(source)) return true;
  return false;
}

export function validateCustomPatterns(sources: string[]): ValidateCustomResult {
  if (!Array.isArray(sources)) {
    return { ok: false, errors: [{ index: 0, reason: 'invalid_regex' }] };
  }
  if (sources.length > MAX_CUSTOM_PATTERNS) {
    return { ok: false, errors: [{ index: MAX_CUSTOM_PATTERNS, reason: 'too_many' }] };
  }
  const errors: Array<{ index: number; reason: ValidateRejection }> = [];
  const compiled: RegExp[] = [];
  for (let i = 0; i < sources.length; i++) {
    const src = sources[i];
    if (typeof src !== 'string' || src.length === 0) {
      errors.push({ index: i, reason: 'empty' });
      continue;
    }
    if (src.length > MAX_PATTERN_LENGTH) {
      errors.push({ index: i, reason: 'too_long' });
      continue;
    }
    if (hasForbiddenConstruct(src)) {
      errors.push({ index: i, reason: 'forbidden_construct' });
      continue;
    }
    if (hasNestedQuantifier(src)) {
      errors.push({ index: i, reason: 'nested_quantifier' });
      continue;
    }
    try {
      compiled.push(new RegExp(src, 'i'));
    } catch {
      errors.push({ index: i, reason: 'invalid_regex' });
    }
  }
  if (errors.length > 0) return { ok: false, errors };
  return { ok: true, compiled };
}

export interface CheckCommandArgs {
  command: string;
  customDenyPatterns?: RegExp[];
  /** When non-empty, command must match at least one (allowlist mode). */
  customAllowPatterns?: RegExp[];
}

export type CheckCommandReason = 'builtin_deny' | 'custom_deny' | 'not_in_allowlist' | 'empty';

export interface CheckCommandResult {
  allowed: boolean;
  reason?: CheckCommandReason;
  /** Name of the built-in pattern (or 'custom') that matched. */
  matched?: string;
}

export function checkCommand(args: CheckCommandArgs): CheckCommandResult {
  const cmd = args.command;
  if (typeof cmd !== 'string' || cmd.trim().length === 0) {
    return { allowed: false, reason: 'empty' };
  }
  for (const p of BUILTIN_DENY_PATTERNS) {
    if (p.regex.test(cmd)) {
      return { allowed: false, reason: 'builtin_deny', matched: p.name };
    }
  }
  if (args.customDenyPatterns) {
    for (let i = 0; i < args.customDenyPatterns.length; i++) {
      if (args.customDenyPatterns[i].test(cmd)) {
        return { allowed: false, reason: 'custom_deny', matched: `custom_deny[${i}]` };
      }
    }
  }
  if (args.customAllowPatterns && args.customAllowPatterns.length > 0) {
    const hit = args.customAllowPatterns.some((r) => r.test(cmd));
    if (!hit) return { allowed: false, reason: 'not_in_allowlist' };
  }
  return { allowed: true };
}
