/**
 * CNF Scanner — Conflict Detector
 * Detects conflicts between config files at different hierarchy levels:
 * settings key conflicts, permission contradictions, hook duplicates.
 * Finding IDs: CA-CNF-NNN
 */

import { readTextFile } from './lib/file-discovery.mjs';
import { finding, scannerResult } from './lib/output.mjs';
import { SEVERITY } from './lib/severity.mjs';
import { parseJson } from './lib/yaml-parser.mjs';
import { truncate } from './lib/string-utils.mjs';

const SCANNER = 'CNF';

// Keys checked separately or not meaningful to compare
const SKIP_KEYS = new Set(['$schema', 'hooks', 'permissions']);

/**
 * Extract the tool name prefix from a permission rule.
 * e.g., "Bash(npm run *)" → "Bash", "Read(src/**)" → "Read"
 * @param {string} rule
 * @returns {{ tool: string, pattern: string }}
 */
function parsePermissionRule(rule) {
  const match = rule.match(/^(\w+)\((.+)\)$/);
  if (match) return { tool: match[1], pattern: match[2] };
  return { tool: rule, pattern: '*' };
}

/**
 * Flatten an object's top-level keys into a simple key→value map.
 * Only first level — we compare top-level settings, not nested.
 * @param {object} obj
 * @returns {Map<string, string>} key → JSON-stringified value
 */
function flattenTopLevel(obj) {
  const map = new Map();
  for (const [key, value] of Object.entries(obj)) {
    if (!SKIP_KEYS.has(key)) {
      map.set(key, JSON.stringify(value));
    }
  }
  return map;
}

/**
 * Collect hooks from a parsed settings or hooks.json object.
 * @param {object} parsed
 * @returns {{ event: string, matcher: string }[]}
 */
function collectHooks(parsed) {
  const hooks = parsed.hooks || parsed;
  if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) return [];

  const result = [];
  for (const [event, handlers] of Object.entries(hooks)) {
    if (!Array.isArray(handlers)) continue;
    for (const handler of handlers) {
      const matcher = typeof handler.matcher === 'string' ? handler.matcher : '*';
      result.push({ event, matcher });
    }
  }
  return result;
}

/**
 * Scan for conflicts across configuration scopes.
 * @param {string} targetPath
 * @param {{ files: import('./lib/file-discovery.mjs').ConfigFile[] }} discovery
 * @returns {Promise<object>}
 */
export async function scan(targetPath, discovery) {
  const start = Date.now();
  const findings = [];

  // Collect settings files
  const settingsFiles = discovery.files.filter(f => f.type === 'settings-json');
  // Collect hooks files
  const hooksFiles = discovery.files.filter(f => f.type === 'hooks-json');

  const totalFiles = settingsFiles.length + hooksFiles.length;

  // Need at least 2 files to detect conflicts
  if (settingsFiles.length < 2 && (settingsFiles.length + hooksFiles.length) < 2) {
    return scannerResult(SCANNER, 'skipped', [], 0, Date.now() - start);
  }

  // --- Settings key conflicts ---
  const settingsByScope = []; // [{ scope, file, keys: Map<key, jsonValue> }]

  for (const file of settingsFiles) {
    const content = await readTextFile(file.absPath);
    if (!content) continue;
    const parsed = parseJson(content);
    if (!parsed) continue;
    settingsByScope.push({
      scope: file.scope,
      file: file.relPath,
      absPath: file.absPath,
      keys: flattenTopLevel(parsed),
      raw: parsed,
    });
  }

  // Compare keys across scopes
  if (settingsByScope.length >= 2) {
    const allKeys = new Set();
    for (const s of settingsByScope) {
      for (const key of s.keys.keys()) allKeys.add(key);
    }

    for (const key of allKeys) {
      const scopesWithKey = settingsByScope.filter(s => s.keys.has(key));
      if (scopesWithKey.length < 2) continue;

      // Check if values differ
      const values = new Set(scopesWithKey.map(s => s.keys.get(key)));
      if (values.size > 1) {
        const details = scopesWithKey
          .map(s => `${s.scope} (${s.file}): ${truncate(s.keys.get(key), 40)}`)
          .join('; ');

        findings.push(finding({
          scanner: SCANNER,
          severity: SEVERITY.medium,
          title: `Settings key conflict: "${key}"`,
          description: `Key "${key}" has different values across scopes. ${details}`,
          file: scopesWithKey[0].absPath,
          evidence: details,
          recommendation: `Verify the "${key}" value is intentionally different across scopes. The most specific scope wins (local > project > user).`,
        }));
      }
    }
  }

  // --- Permission conflicts ---
  for (let i = 0; i < settingsByScope.length; i++) {
    for (let j = i + 1; j < settingsByScope.length; j++) {
      const a = settingsByScope[i];
      const b = settingsByScope[j];

      const aPerms = a.raw.permissions || {};
      const bPerms = b.raw.permissions || {};

      const aAllow = Array.isArray(aPerms.allow) ? aPerms.allow : [];
      const aDeny = Array.isArray(aPerms.deny) ? aPerms.deny : [];
      const bAllow = Array.isArray(bPerms.allow) ? bPerms.allow : [];
      const bDeny = Array.isArray(bPerms.deny) ? bPerms.deny : [];

      // Check: allow in A, deny in B (and vice versa)
      for (const allowRule of aAllow) {
        const { tool: aTool, pattern: aPattern } = parsePermissionRule(allowRule);
        for (const denyRule of bDeny) {
          const { tool: dTool, pattern: dPattern } = parsePermissionRule(denyRule);
          if (aTool === dTool && (aPattern === dPattern || aPattern === '*' || dPattern === '*')) {
            findings.push(finding({
              scanner: SCANNER,
              severity: SEVERITY.high,
              title: 'Permission allow/deny conflict',
              description: `"${allowRule}" is allowed in ${a.scope} (${a.file}) but denied in ${b.scope} (${b.file}).`,
              file: a.absPath,
              evidence: `allow: "${allowRule}" (${a.scope}) vs deny: "${denyRule}" (${b.scope})`,
              recommendation: 'Resolve the conflict. Deny always wins, but the conflicting allow rule is misleading.',
            }));
          }
        }
      }

      // Reverse: allow in B, deny in A
      for (const allowRule of bAllow) {
        const { tool: bTool, pattern: bPattern } = parsePermissionRule(allowRule);
        for (const denyRule of aDeny) {
          const { tool: dTool, pattern: dPattern } = parsePermissionRule(denyRule);
          if (bTool === dTool && (bPattern === dPattern || bPattern === '*' || dPattern === '*')) {
            findings.push(finding({
              scanner: SCANNER,
              severity: SEVERITY.high,
              title: 'Permission allow/deny conflict',
              description: `"${allowRule}" is allowed in ${b.scope} (${b.file}) but denied in ${a.scope} (${a.file}).`,
              file: b.absPath,
              evidence: `allow: "${allowRule}" (${b.scope}) vs deny: "${denyRule}" (${a.scope})`,
              recommendation: 'Resolve the conflict. Deny always wins, but the conflicting allow rule is misleading.',
            }));
          }
        }
      }
    }
  }

  // --- Hook duplicates (across settings + hooks.json files) ---
  const hookSources = []; // [{ event, matcher, source }]

  for (const s of settingsByScope) {
    if (s.raw.hooks) {
      for (const h of collectHooks(s.raw)) {
        hookSources.push({ ...h, source: `${s.scope}:${s.file}` });
      }
    }
  }

  for (const file of hooksFiles) {
    const content = await readTextFile(file.absPath);
    if (!content) continue;
    const parsed = parseJson(content);
    if (!parsed) continue;
    const hookData = parsed.hooks || parsed;
    for (const h of collectHooks(hookData)) {
      hookSources.push({ ...h, source: `hooks:${file.relPath}` });
    }
  }

  // Group by event:matcher
  const hookGroups = new Map();
  for (const h of hookSources) {
    const key = `${h.event}:${h.matcher}`;
    if (!hookGroups.has(key)) hookGroups.set(key, []);
    hookGroups.get(key).push(h.source);
  }

  for (const [key, sources] of hookGroups) {
    // Only flag duplicates from DIFFERENT sources
    const uniqueSources = [...new Set(sources)];
    if (uniqueSources.length >= 2) {
      const [event, matcher] = key.split(':');
      findings.push(finding({
        scanner: SCANNER,
        severity: SEVERITY.low,
        title: 'Duplicate hook definition',
        description: `Hook "${event}" with matcher "${matcher}" is defined in ${uniqueSources.length} sources.`,
        evidence: uniqueSources.join(', '),
        recommendation: 'Consolidate hook definitions to avoid unexpected execution order.',
      }));
    }
  }

  return scannerResult(SCANNER, 'ok', findings, totalFiles, Date.now() - start);
}
