'use strict';

const crypto = require('node:crypto');
const path = require('node:path');
const { checkPermission } = require('../tools/permission-checker');
const { analyzeShellCommand, initParser: initShellParser, bashArityPrefix } = require('../tools/shell-analyzer');

// Shell builtins that only navigate; skipped when choosing the approval card's
// display pattern so a leading `cd` doesn't label the request as "cd *".
const _DISPLAY_NAV = new Set(['cd', 'pushd', 'popd', 'dirs']);

// Pick the pattern shown on the approval card (and stored for "always" rules)
// for a compound run_shell command: the first meaningful (non-navigation)
// clause, so `cd /x && git push` is labelled `git push *`, not `cd *`.
function pickShellDisplayPattern(clauses, command) {
  const meaningful = clauses.find((t) => Array.isArray(t) && t.length > 0 && !_DISPLAY_NAV.has(t[0]))
    || (clauses.length > 0 ? clauses[0] : null);
  if (meaningful && meaningful.length > 0) {
    return `${bashArityPrefix(meaningful).join(' ')} *`;
  }
  return `${String(command).split(/\s+/)[0] || '*'} *`;
}
const { PermissionRulesStore } = require('./permission-rules-store');
const { inferPatchPaths } = require('./snapshot-service');

const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
// Timeout semantics for a permission request:
//   > 0                  -> auto-deny after N ms (used for headless/automated runs)
//   === 0                -> headless: resolve immediately via headlessPolicy (no prompt)
//   WAIT_FOR_REPLY (< 0) -> interactive: never auto-deny; stay pending until the user
//                           replies or the session is cleared (matches Claude Code /
//                           opencode, which wait for the user instead of timing out).
// The < 0 case needs no special branch: ask() only arms a timer when the effective
// timeout is > 0, and authorize()'s headless shortcut matches exactly === 0, so a
// negative value naturally falls through to a no-timer wait.
const WAIT_FOR_REPLY = -1;
const _sharedPending = new Map();

class PermissionService {
  constructor({
    events = null,
    rulesStore = null,
    timeoutMs = DEFAULT_TIMEOUT_MS,
    headlessPolicy = 'reject',
    evaluatePermission = checkPermission,
  } = {}) {
    this.events = events;
    this.rulesStore = rulesStore || new PermissionRulesStore();
    this.timeoutMs = timeoutMs;
    this.headlessPolicy = headlessPolicy;
    this.evaluatePermission = evaluatePermission;
    this.pending = _sharedPending;
  }

  async authorize({ sessionId = '', tool, input = {}, cwd = '', projectRoot = '', mode = '', metadata = {}, timeoutMs, headless = false } = {}) {
    const permissionContextId = metadata.permissionContextId || metadata.permission_context_id || createPermissionContextId();
    const requests = await buildToolPermissionRequests(tool, input, {
      cwd,
      projectRoot: projectRoot || cwd,
      sessionId,
      mode,
      metadata: { ...metadata, permissionContextId },
    });
    if (requests.length === 0) return { decision: 'allow', source: 'default', reason: 'No permission required', permissionContextId };

    for (const request of requests) {
      const evaluated = await this.evaluatePermission(request.check);
      if (evaluated.decision === 'allow') continue;
      if (evaluated.decision === 'deny') {
        return { ...evaluated, request, permissionContextId };
      }

      const always = this.rulesStore.findAlways({
        projectRoot: request.projectRoot,
        permission: request.permission,
        pattern: request.pattern,
        tool: request.tool,
        fingerprint: request.fingerprint,
      });
      if (always) {
        continue;
      }

      if (headless || (timeoutMs ?? this.timeoutMs) === 0) {
        return {
          decision: this.headlessPolicy === 'allow' ? 'allow' : 'deny',
          source: 'headless',
          reason: `Permission required for ${request.permission}/${request.pattern}`,
          request,
          permissionContextId,
        };
      }

      const reply = await this.ask({
        ...request,
        sessionId,
        metadata: { ...metadata, ...request.metadata, evaluated },
        permissionContextId,
        timeoutMs,
      });
      if (reply.decision !== 'allow') return { ...reply, request, permissionContextId };
    }

    return { decision: 'allow', source: 'permissions', reason: 'Permission checks passed', permissionContextId };
  }

  ask({ sessionId = '', permission, pattern = '*', patterns = null, tool = '', fingerprint = '', metadata = {}, permissionContextId = '', timeoutMs } = {}) {
    if (!permission) throw new Error('permission is required');
    const id = `perm_${crypto.randomUUID().slice(0, 8)}`;
    const contextId = permissionContextId || metadata.permissionContextId || metadata.permission_context_id || createPermissionContextId();
    const effectiveTimeout = timeoutMs ?? this.timeoutMs;

    return new Promise((resolve) => {
      const timer = effectiveTimeout > 0
        ? setTimeout(() => this._expire(id), effectiveTimeout)
        : null;
      const entry = {
        id,
        sessionId,
        permission,
        pattern,
        patterns: patterns || [pattern],
        tool,
        fingerprint: fingerprint || metadata.fingerprint || '',
        permissionContextId: contextId,
        metadata: { ...metadata, permissionContextId: contextId },
        createdAt: Date.now(),
        timer,
        resolve,
      };
      this.pending.set(id, entry);
      this._emit('permission.asked', publicEntry(entry));
    });
  }

  reply({ requestId, reply, message = '' } = {}) {
    const entry = this.pending.get(requestId);
    if (!entry) return false;
    const decision = reply === 'once' || reply === 'always' ? 'allow' : 'deny';
    this._resolveEntry(entry, { decision, reply, message, source: 'user' });
    if (reply === 'always') {
      const rule = this.rulesStore.addAlways({
        projectRoot: entry.metadata.projectRoot || '',
        permission: entry.permission,
        pattern: entry.pattern,
        tool: entry.tool,
        fingerprint: entry.fingerprint || entry.metadata.fingerprint || '',
        metadata: { ...entry.metadata, message },
      });
      this._resolveCompatibleAlways(entry, rule, message);
    }
    return true;
  }

  list({ sessionId = '' } = {}) {
    const out = [];
    for (const entry of this.pending.values()) {
      if (sessionId && entry.sessionId !== sessionId) continue;
      out.push(publicEntry(entry));
    }
    return out;
  }

  clear({ sessionId = '' } = {}) {
    for (const entry of [...this.pending.values()]) {
      if (sessionId && entry.sessionId !== sessionId) continue;
      this._resolveEntry(entry, { decision: 'deny', reply: 'reject', message: 'Permission request cleared', source: 'clear' });
    }
  }

  _resolveCompatibleAlways(sourceEntry, rule, message) {
    for (const entry of [...this.pending.values()]) {
      if (entry.id === sourceEntry.id) continue;
      if (entry.permission !== sourceEntry.permission) continue;
      if (entry.tool !== sourceEntry.tool) continue;
      if ((entry.metadata.projectRoot || '') !== (sourceEntry.metadata.projectRoot || '')) continue;
      if (entry.pattern !== sourceEntry.pattern) continue;
      if ((entry.fingerprint || '') !== (sourceEntry.fingerprint || '')) continue;
      this._resolveEntry(entry, { decision: 'allow', reply: 'always', message, source: 'always', rule });
    }
  }

  _resolveEntry(entry, result) {
    if (entry.timer) clearTimeout(entry.timer);
    this.pending.delete(entry.id);
    const resolved = { ...result, permissionContextId: entry.permissionContextId || entry.metadata?.permissionContextId || '' };
    this._emit('permission.replied', { ...publicEntry(entry), result: resolved });
    entry.resolve(resolved);
  }

  _expire(id) {
    const entry = this.pending.get(id);
    if (!entry) return;
    this._resolveEntry(entry, {
      decision: 'deny',
      reply: 'timeout',
      message: 'Permission request timed out',
      source: 'timeout',
    });
  }

  _emit(type, payload) {
    if (this.events?.emit) this.events.emit(type, payload);
  }
}

async function buildToolPermissionRequests(tool, input = {}, { cwd = '', projectRoot = '', sessionId = '', mode = '', metadata = {} } = {}) {
  const root = path.resolve(projectRoot || cwd || process.cwd());
  const base = { sessionId, tool, projectRoot: root, metadata: { ...metadata, projectRoot: root } };
  if (!tool) return [];

  if (tool === 'run_shell') {
    const command = input.command || '';
    await initShellParser();
    const analysis = await analyzeShellCommand(command, input.cwd || cwd || root);
    // commandTokens is an array of per-clause token arrays (split on && ; | etc).
    // The whole compound command must be permission-checked clause-by-clause, so
    // pass the full list through to checkPermission; keep the first clause in
    // metadata.commandTokens so the approval fingerprint stays stable.
    const commandClauses = Array.isArray(analysis.commandTokens) ? analysis.commandTokens : [];
    const commandTokens = commandClauses.length > 0 ? commandClauses[0] : undefined;
    const pattern = pickShellDisplayPattern(commandClauses, command);
    return withApprovalFingerprints([{
      ...base,
      permission: 'bash',
      pattern,
      metadata: { ...base.metadata, command, commandTokens },
      check: { tool: 'run_shell', command, commandTokens, commandClauses, projectPath: root, sessionId, mode },
    }], input);
  }

  if (tool === 'read_file') {
    const filePath = resolveInputPath(input.file_path || input.path || '', root);
    return withApprovalFingerprints([fileRequest({ ...base, permission: 'read', pattern: filePath, filePath, mode })], input);
  }

  if (tool === 'write_file' || tool === 'edit_file' || tool === 'multi_edit') {
    const filePath = resolveInputPath(input.file_path || input.path || '', root);
    return withApprovalFingerprints([fileRequest({ ...base, tool, permission: 'edit', pattern: filePath, filePath, mode })], input);
  }

  if (tool === 'apply_patch') {
    const files = inferPatchPaths(input.patch_text || input.patch || '', root);
    return withApprovalFingerprints(files.map((filePath) => fileRequest({ ...base, tool, permission: 'edit', pattern: filePath, filePath, mode })), input);
  }

  if (tool === 'mcp_call' || String(tool || '').startsWith('mcp__')) {
    const pattern = tool === 'mcp_call'
      ? `${input.server || '*'}.${input.tool || '*'}`
      : String(tool);
    return withApprovalFingerprints([{
      ...base,
      tool,
      permission: 'mcp',
      pattern,
      check: { tool: 'mcp_call', command: pattern, args: input.arguments || input, projectPath: root, sessionId, mode },
    }], input);
  }

  if (tool === 'skill' || tool === 'load_skill' || String(tool || '').startsWith('skill__')) {
    const pattern = tool === 'skill' || tool === 'load_skill'
      ? (input.name || '*')
      : String(tool).replace(/^skill__/, '').replace(/_/g, '-');
    return withApprovalFingerprints([{
      ...base,
      tool,
      permission: 'skill',
      pattern,
      check: { tool: 'skill', command: pattern, args: input.arguments || input, projectPath: root, sessionId, mode },
    }], input);
  }

  if (['applescript', 'claude_code', 'mail_send', 'mail_reply', 'slack_send_message'].includes(tool)) {
    return withApprovalFingerprints([{
      ...base,
      permission: tool,
      pattern: '*',
      check: { tool, command: '', args: [], projectPath: root, sessionId, mode },
    }], input);
  }

  return [];
}

function fileRequest({ sessionId, tool, projectRoot, permission, pattern, filePath, mode, metadata }) {
  const checkTool = permission === 'read' ? 'read_file' : 'edit_file';
  return {
    sessionId,
    tool,
    projectRoot,
    permission,
    pattern,
    metadata: { ...metadata, filePath, projectRoot },
    check: { tool: checkTool, command: filePath, args: [], projectPath: projectRoot, sessionId, mode },
  };
}

function withApprovalFingerprints(requests, input) {
  return requests.map((request) => {
    const fingerprintInput = approvalFingerprintInput(request, input);
    const fingerprint = approvalFingerprint(fingerprintInput);
    return {
      ...request,
      fingerprint,
      metadata: {
        ...request.metadata,
        fingerprint,
        fingerprintInput,
      },
    };
  });
}

function approvalFingerprintInput(request, input = {}) {
  const base = {
    tool: request.tool || '',
    permission: request.permission || '',
    pattern: request.pattern || '',
    projectRoot: request.projectRoot || '',
  };
  if (request.tool === 'run_shell') {
    return {
      ...base,
      command: request.metadata?.command || input.command || '',
      commandTokens: request.metadata?.commandTokens || [],
    };
  }
  if (request.tool === 'apply_patch') {
    return {
      ...base,
      patchHash: hashValue(input.patch_text || input.patch || ''),
    };
  }
  if (request.permission === 'read' || request.permission === 'edit') {
    return {
      ...base,
      filePath: request.metadata?.filePath || request.pattern || '',
      inputHash: request.permission === 'edit' ? hashValue(input) : '',
    };
  }
  if (request.permission === 'mcp') {
    return {
      ...base,
      server: input.server || '',
      mcpTool: input.tool || request.pattern || '',
      argumentsHash: hashValue(input.arguments || input),
    };
  }
  return {
    ...base,
    inputHash: hashValue(input),
  };
}

function approvalFingerprint(value) {
  return `approval:${hashValue(value).slice(0, 24)}`;
}

function hashValue(value) {
  return crypto.createHash('sha256').update(stableStringify(value)).digest('hex');
}

function stableStringify(value) {
  if (value === null || typeof value !== 'object') return JSON.stringify(value);
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
  const keys = Object.keys(value).sort();
  return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
}

function resolveInputPath(filePath, root) {
  if (!filePath) return '';
  return path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(root, filePath);
}

function publicEntry(entry) {
  return {
    id: entry.id,
    sessionId: entry.sessionId,
    permissionContextId: entry.permissionContextId || entry.metadata?.permissionContextId || '',
    permission: entry.permission,
    pattern: entry.pattern,
    patterns: entry.patterns,
    tool: entry.tool,
    fingerprint: entry.fingerprint || entry.metadata?.fingerprint || '',
    metadata: entry.metadata,
    createdAt: entry.createdAt,
  };
}

function createPermissionContextId() {
  return `permctx_${crypto.randomUUID().slice(0, 8)}`;
}

function getDefaultPermissionService() {
  return new PermissionService();
}

function clearPendingPermissions() {
  for (const entry of _sharedPending.values()) {
    if (entry.timer) clearTimeout(entry.timer);
  }
  _sharedPending.clear();
}

module.exports = {
  PermissionService,
  buildToolPermissionRequests,
  approvalFingerprint,
  approvalFingerprintInput,
  createPermissionContextId,
  getDefaultPermissionService,
  clearPendingPermissions,
  DEFAULT_TIMEOUT_MS,
  WAIT_FOR_REPLY,
};
