import path from 'node:path';
import process from 'node:process';

const FILE_KEYS = new Set([
  'dirPath',
  'file',
  'filePath',
  'filePaths',
  'files',
  'from',
  'path',
  'paths',
  'targetPath',
  'to',
  'uri',
  'uris',
]);

const escapeRegex = (value) => value.replace(/[.+?^${}()|[\]\\]/g, '\\$&');

const normalizePath = (value) => {
  if (typeof value !== 'string') return '';
  let next = value.trim();
  if (next.startsWith('file://')) {
    try {
      next = decodeURIComponent(new URL(next).pathname);
    } catch {
      return '';
    }
  }
  if (/^\/[a-z]:/i.test(next)) next = next.slice(1);
  return path.normalize(next).replace(/\\/g, '/').toLowerCase();
};

/**
 * Creates a hook entrypoint that normalizes stdin and writes a platform-neutral response.
 * @param {(context: ReturnType<typeof normalizeContext>) => Promise<object> | object} handler
 */
export async function createHook(handler) {
  try {
    const raw = await readStdin();
    const result = (await handler(normalizeContext(raw))) ?? { decision: 'allow' };
    respond(result);
  } catch (error) {
    respond({ decision: 'deny', reason: error instanceof Error ? error.message : String(error) });
  }
}

/**
 * Reads and parses the full stdin payload as JSON.
 * @returns {Promise<object>}
 */
export function readStdin() {
  return new Promise((resolve, reject) => {
    let input = '';
    process.stdin.setEncoding('utf8');
    process.stdin.on('data', (chunk) => {
      input += chunk;
    });
    process.stdin.on('end', () => {
      if (!input.trim()) return resolve({});
      try {
        resolve(JSON.parse(input));
      } catch (error) {
        reject(error);
      }
    });
    process.stdin.on('error', reject);
  });
}

/**
 * Normalizes platform-specific hook payloads into one shared shape.
 * @param {object} raw
 */
export function normalizeContext(raw = {}) {
  const platform = raw.tool_name
    ? 'claude'
    : raw.toolName
      ? 'copilot'
      : raw.tool
        ? 'copilot-cli'
        : 'copilot';
  const toolInput =
    raw.toolInput ?? raw.tool_input ?? raw.input ?? raw.arguments ?? raw.params ?? {};
  return {
    raw,
    platform,
    event: raw.event ?? raw.hookEvent ?? raw.hook_event ?? 'SessionStart',
    toolName: raw.toolName ?? raw.tool_name ?? raw.tool ?? '',
    toolInput,
    filePaths: extractFilePaths(toolInput),
    env: raw.env ?? raw.environment ?? raw.processEnv ?? {},
    cwd: raw.cwd ?? raw.workingDirectory ?? raw.working_directory ?? process.cwd(),
    matchesPattern,
    normalizePath,
  };
}

/**
 * Writes a normalized hook response to stdout and sets the exit code.
 * @param {{ decision?: 'allow' | 'deny', reason?: string, additionalContext?: string, continue?: boolean }} result
 */
export function respond(result = { decision: 'allow' }) {
  const decision = result.decision ?? 'allow';
  const payload =
    decision === 'deny'
      ? { decision: 'deny', reason: result.reason ?? 'Hook denied request.' }
      : {
          ...(result.additionalContext ? { additionalContext: result.additionalContext } : {}),
          ...(result.continue === false ? { continue: false } : {}),
        };
  if (Object.keys(payload).length > 0) process.stdout.write(`${JSON.stringify(payload)}\n`);
  process.exitCode = decision === 'deny' ? 2 : 0;
}

/**
 * Extracts file paths from common tool input field names.
 * @param {unknown} toolInput
 * @returns {string[]}
 */
export function extractFilePaths(toolInput) {
  const found = new Set();
  const visit = (value, key = '') => {
    if (Array.isArray(value)) {
      value.forEach((item) => {
        visit(item, key);
      });
      return;
    }
    if (value && typeof value === 'object') {
      Object.entries(value).forEach(([nextKey, nextValue]) => {
        visit(nextValue, nextKey);
      });
      return;
    }
    if (typeof value === 'string' && FILE_KEYS.has(key)) found.add(normalizePath(value));
  };
  visit(toolInput);
  return [...found].filter(Boolean);
}

/**
 * Matches a normalized path against simple wildcard and directory-prefix patterns.
 * @param {string} filePath
 * @param {string[]} patterns
 */
export function matchesPattern(filePath, patterns) {
  const target = normalizePath(filePath);
  return patterns.some((pattern) => {
    const normalizedPattern = normalizePath(pattern);
    if (!normalizedPattern) return false;
    if (normalizedPattern.endsWith('/')) {
      const prefix = normalizedPattern.slice(0, -1);
      return target === prefix || target.includes(`/${prefix}/`) || target.startsWith(`${prefix}/`);
    }
    return new RegExp(`(^|/)${normalizedPattern.split('*').map(escapeRegex).join('.*')}$`).test(
      target,
    );
  });
}
