/**
 * GAP Scanner — Feature Gap Scanner
 * Compares actual configuration against complete Claude Code feature register.
 * 25 gap dimensions across 4 tiers. Always runs with includeGlobal: true.
 * Finding IDs: CA-GAP-NNN
 */

import { resolve } from 'node:path';
import { readTextFile, discoverConfigFiles } from './lib/file-discovery.mjs';
import { finding, scannerResult } from './lib/output.mjs';
import { SEVERITY } from './lib/severity.mjs';
import { findImports, parseJson, parseFrontmatter } from './lib/yaml-parser.mjs';

const SCANNER = 'GAP';

/**
 * @typedef {object} GapCheck
 * @property {string} id - Short identifier (t1_1 through t4_5)
 * @property {string} tier - t1|t2|t3|t4
 * @property {string} title - Human-readable title
 * @property {string} recommendation - What to do
 * @property {(ctx: CheckContext) => Promise<boolean>} check - Returns true if feature IS present
 */

/**
 * @typedef {object} CheckContext
 * @property {import('./lib/file-discovery.mjs').ConfigFile[]} files
 * @property {string} targetPath
 * @property {Map<string, object>} parsedSettings - scope → parsed JSON
 * @property {Map<string, string>} fileContents - absPath → content
 */

/**
 * Check if a file belongs to the target project (vs global ~/.claude/).
 * Needed because scope classification can be 'plugin' when running inside ~/.claude/plugins/.
 * @param {CheckContext} ctx
 * @param {import('./lib/file-discovery.mjs').ConfigFile} f
 * @returns {boolean}
 */
function isTargetLocal(ctx, f) {
  return f.absPath.startsWith(ctx.targetPath);
}

const TIER_SEVERITY = {
  t1: SEVERITY.medium,
  t2: SEVERITY.low,
  t3: SEVERITY.info,
  t4: SEVERITY.info,
};

/**
 * Lazily read and cache file content.
 * @param {CheckContext} ctx
 * @param {string} absPath
 * @returns {Promise<string|null>}
 */
async function getContent(ctx, absPath) {
  if (ctx.fileContents.has(absPath)) return ctx.fileContents.get(absPath);
  const content = await readTextFile(absPath);
  ctx.fileContents.set(absPath, content);
  return content;
}

/**
 * Check if any settings file has a specific key.
 * @param {CheckContext} ctx
 * @param {string} key
 * @returns {boolean}
 */
function anySettingsHas(ctx, key) {
  for (const parsed of ctx.parsedSettings.values()) {
    if (parsed && key in parsed) return true;
  }
  return false;
}

/**
 * Get a value from any settings file (first match).
 * @param {CheckContext} ctx
 * @param {string} key
 * @returns {*}
 */
function getSettingsValue(ctx, key) {
  for (const parsed of ctx.parsedSettings.values()) {
    if (parsed && key in parsed) return parsed[key];
  }
  return undefined;
}

/** @type {GapCheck[]} */
const GAP_CHECKS = [
  // --- Tier 1: Foundation ---
  {
    id: 't1_1', tier: 't1',
    title: 'No CLAUDE.md file',
    recommendation: 'Create a CLAUDE.md at the project root with project-specific instructions, commands, and architecture.',
    check: async (ctx) => ctx.files.some(f => f.type === 'claude-md' && isTargetLocal(ctx, f)),
  },
  {
    id: 't1_2', tier: 't1',
    title: 'No permissions configured',
    recommendation: 'Add permissions.allow and permissions.deny in .claude/settings.json to control tool access.',
    check: async (ctx) => {
      for (const parsed of ctx.parsedSettings.values()) {
        if (parsed?.permissions && (parsed.permissions.allow?.length > 0 || parsed.permissions.deny?.length > 0)) {
          return true;
        }
      }
      return false;
    },
  },
  {
    id: 't1_3', tier: 't1',
    title: 'No hooks configured',
    recommendation: 'Add at least one hook (e.g., PreToolUse for security, Stop for session summaries). See knowledge/hook-events-reference.md.',
    check: async (ctx) => {
      if (ctx.files.some(f => f.type === 'hooks-json')) return true;
      for (const parsed of ctx.parsedSettings.values()) {
        if (parsed?.hooks && typeof parsed.hooks === 'object' && !Array.isArray(parsed.hooks)) {
          return Object.keys(parsed.hooks).length > 0;
        }
      }
      return false;
    },
  },
  {
    id: 't1_4', tier: 't1',
    title: 'No custom skills or commands',
    recommendation: 'Create project-specific skills in .claude/skills/ or commands in .claude/commands/ to automate repetitive workflows.',
    check: async (ctx) => ctx.files.some(f => f.type === 'skill-md' || f.type === 'command-md'),
  },
  {
    id: 't1_5', tier: 't1',
    title: 'No MCP servers configured',
    recommendation: 'Add a .mcp.json at the project root to configure MCP servers for enhanced tool access.',
    check: async (ctx) => ctx.files.some(f => f.type === 'mcp-json'),
  },

  // --- Tier 2: Configuration Depth ---
  {
    id: 't2_1', tier: 't2',
    title: 'Settings only at one scope',
    recommendation: 'Use all 3 settings scopes: ~/.claude/settings.json (user), .claude/settings.json (project), .claude/settings.local.json (local/personal).',
    check: async (ctx) => {
      const localSettings = ctx.files.filter(f => f.type === 'settings-json' && isTargetLocal(ctx, f));
      const hasGlobal = ctx.files.some(f => f.type === 'settings-json' && !isTargetLocal(ctx, f));
      return (localSettings.length >= 2) || (localSettings.length >= 1 && hasGlobal);
    },
  },
  {
    id: 't2_2', tier: 't2',
    title: 'CLAUDE.md not modular',
    recommendation: 'Use @imports or .claude/rules/ to split large CLAUDE.md files into focused modules.',
    check: async (ctx) => {
      // Has rules files OR has @imports in any CLAUDE.md
      if (ctx.files.some(f => f.type === 'rule')) return true;
      for (const file of ctx.files.filter(f => f.type === 'claude-md')) {
        const content = await getContent(ctx, file.absPath);
        if (content && findImports(content).length > 0) return true;
      }
      return false;
    },
  },
  {
    id: 't2_3', tier: 't2',
    title: 'No path-scoped rules',
    recommendation: 'Create .claude/rules/*.md with paths: frontmatter to apply rules only to matching files.',
    check: async (ctx) => {
      for (const file of ctx.files.filter(f => f.type === 'rule')) {
        const content = await getContent(ctx, file.absPath);
        if (content) {
          const { frontmatter } = parseFrontmatter(content);
          if (frontmatter && (frontmatter.paths || frontmatter.globs)) return true;
        }
      }
      return false;
    },
  },
  {
    id: 't2_4', tier: 't2',
    title: 'Auto-memory explicitly disabled',
    recommendation: 'Enable auto-memory by removing autoMemoryEnabled: false from settings.',
    check: async (ctx) => {
      // Present (gap) only if explicitly disabled
      const val = getSettingsValue(ctx, 'autoMemoryEnabled');
      return val !== false;
    },
  },
  {
    id: 't2_5', tier: 't2',
    title: 'Low hook diversity',
    recommendation: 'Use hooks across 3+ events (e.g., SessionStart, PreToolUse, Stop) for comprehensive automation.',
    check: async (ctx) => {
      const events = new Set();
      for (const parsed of ctx.parsedSettings.values()) {
        if (parsed?.hooks && typeof parsed.hooks === 'object' && !Array.isArray(parsed.hooks)) {
          for (const event of Object.keys(parsed.hooks)) events.add(event);
        }
      }
      for (const file of ctx.files.filter(f => f.type === 'hooks-json')) {
        const content = await getContent(ctx, file.absPath);
        if (content) {
          const parsed = parseJson(content);
          const hookData = parsed?.hooks || parsed;
          if (hookData && typeof hookData === 'object' && !Array.isArray(hookData)) {
            for (const event of Object.keys(hookData)) events.add(event);
          }
        }
      }
      return events.size >= 3;
    },
  },
  {
    id: 't2_6', tier: 't2',
    title: 'No custom subagents',
    recommendation: 'Create custom agents in .claude/agents/ or ~/.claude/agents/ with specialized tools and model selection.',
    check: async (ctx) => ctx.files.some(f => f.type === 'agent-md'),
  },
  {
    id: 't2_7', tier: 't2',
    title: 'No model configuration',
    recommendation: 'Set model preferences in settings.json (model, modelOverrides) for cost/quality optimization.',
    check: async (ctx) => anySettingsHas(ctx, 'model') || anySettingsHas(ctx, 'modelOverrides'),
  },

  // --- Tier 3: Advanced Features ---
  {
    id: 't3_1', tier: 't3',
    title: 'No status line configured',
    recommendation: 'Configure statusLine in settings.json to show context window usage, cost, and model info.',
    check: async (ctx) => anySettingsHas(ctx, 'statusLine'),
  },
  {
    id: 't3_2', tier: 't3',
    title: 'No custom keybindings',
    recommendation: 'Create ~/.claude/keybindings.json to customize keyboard shortcuts (e.g., bind chat:newline to Shift+Enter).',
    check: async (ctx) => ctx.files.some(f => f.type === 'keybindings-json'),
  },
  {
    id: 't3_3', tier: 't3',
    title: 'Using default output style',
    recommendation: 'Try "Explanatory" or "Learning" output styles, or create custom styles in .claude/output-styles/.',
    check: async (ctx) => anySettingsHas(ctx, 'outputStyle'),
  },
  {
    id: 't3_4', tier: 't3',
    title: 'No worktree workflow',
    recommendation: 'Use --worktree for parallel feature development. Configure worktree.symlinkDirectories for node_modules.',
    check: async (ctx) => anySettingsHas(ctx, 'worktree'),
  },
  {
    id: 't3_5', tier: 't3',
    title: 'No advanced skill frontmatter',
    recommendation: 'Use disable-model-invocation, context:fork, or argument-hint in skill frontmatter for better control.',
    check: async (ctx) => {
      for (const file of ctx.files.filter(f => f.type === 'skill-md')) {
        const content = await getContent(ctx, file.absPath);
        if (content) {
          const { frontmatter } = parseFrontmatter(content);
          if (frontmatter && (
            frontmatter.disable_model_invocation ||
            frontmatter.context === 'fork' ||
            frontmatter.argument_hint
          )) return true;
        }
      }
      return false;
    },
  },
  {
    id: 't3_6', tier: 't3',
    title: 'No subagent isolation',
    recommendation: 'Use isolation: worktree in agent frontmatter for safe parallel development.',
    check: async (ctx) => {
      for (const file of ctx.files.filter(f => f.type === 'agent-md')) {
        const content = await getContent(ctx, file.absPath);
        if (content) {
          const { frontmatter } = parseFrontmatter(content);
          if (frontmatter && frontmatter.isolation === 'worktree') return true;
        }
      }
      return false;
    },
  },
  {
    id: 't3_7', tier: 't3',
    title: 'No dynamic skill context',
    recommendation: 'Use !`command` syntax in skills to inject dynamic context (e.g., !`git branch --show-current`).',
    check: async (ctx) => {
      for (const file of ctx.files.filter(f => f.type === 'skill-md' || f.type === 'command-md')) {
        const content = await getContent(ctx, file.absPath);
        if (content && /!`[^`]+`/.test(content)) return true;
      }
      return false;
    },
  },
  {
    id: 't3_8', tier: 't3',
    title: 'No autoMode classifier',
    recommendation: 'Configure autoMode in user/local settings with environment context and allow/deny rules.',
    check: async (ctx) => anySettingsHas(ctx, 'autoMode'),
  },

  // --- Tier 4: Team/Enterprise ---
  {
    id: 't4_1', tier: 't4',
    title: 'No project .mcp.json in git',
    recommendation: 'Add .mcp.json to git so the team shares MCP server configuration.',
    check: async (ctx) => ctx.files.some(f => f.type === 'mcp-json' && isTargetLocal(ctx, f)),
  },
  {
    id: 't4_2', tier: 't4',
    title: 'No custom plugin',
    recommendation: 'Package reusable skills, agents, and hooks as a Claude Code plugin with .claude-plugin/plugin.json.',
    check: async (ctx) => ctx.files.some(f => f.type === 'plugin-json'),
  },
  {
    id: 't4_3', tier: 't4',
    title: 'Agent teams not enabled',
    recommendation: 'Enable agent teams with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 for parallel multi-agent workflows.',
    check: async (ctx) => {
      for (const parsed of ctx.parsedSettings.values()) {
        const env = parsed?.env;
        if (env && env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === '1') return true;
      }
      return !!process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
    },
  },
  {
    id: 't4_4', tier: 't4',
    title: 'No managed settings',
    recommendation: 'Use managed-settings.json for organization-wide policy enforcement.',
    check: async (ctx) => ctx.files.some(f => f.scope === 'managed'),
  },
  {
    id: 't4_5', tier: 't4',
    title: 'No LSP plugins',
    recommendation: 'Add .lsp.json for real-time code intelligence from language servers.',
    check: async (ctx) => ctx.files.some(f => f.relPath.endsWith('.lsp.json')),
  },
];

/**
 * Scan for feature gaps against Claude Code feature register.
 * @param {string} targetPath
 * @param {{ files: import('./lib/file-discovery.mjs').ConfigFile[] }} sharedDiscovery - Used when provided with files; otherwise runs own discovery with includeGlobal
 * @returns {Promise<object>}
 */
export async function scan(targetPath, sharedDiscovery) {
  const start = Date.now();
  const findings = [];

  // Use shared discovery if it has files (e.g. from full-machine mode), otherwise run own
  const discovery = (sharedDiscovery && sharedDiscovery.files && sharedDiscovery.files.length > 0)
    ? sharedDiscovery
    : await discoverConfigFiles(resolve(targetPath), { includeGlobal: true });

  // Parse all settings files upfront
  const parsedSettings = new Map();
  for (const file of discovery.files.filter(f => f.type === 'settings-json')) {
    const content = await readTextFile(file.absPath);
    if (content) {
      const parsed = parseJson(content);
      parsedSettings.set(`${file.scope}:${file.relPath}`, parsed);
    }
  }

  const ctx = {
    files: discovery.files,
    targetPath: resolve(targetPath),
    parsedSettings,
    fileContents: new Map(),
  };

  for (const gap of GAP_CHECKS) {
    const present = await gap.check(ctx);
    if (!present) {
      findings.push(finding({
        scanner: SCANNER,
        severity: TIER_SEVERITY[gap.tier],
        title: gap.title,
        description: `Feature gap: ${gap.title}. ${gap.recommendation}`,
        recommendation: gap.recommendation,
        category: gap.tier,
      }));
    }
  }

  const filesScanned = discovery.files.length;
  return scannerResult(SCANNER, 'ok', findings, filesScanned, Date.now() - start);
}

/**
 * Group GAP findings into impact categories for opportunity-based display.
 * @param {object[]} findings - GAP scanner findings (each has .category = t1|t2|t3|t4)
 * @returns {{ highImpact: object[], mediumImpact: object[], explore: object[] }}
 */
export function opportunitySummary(findings) {
  const highImpact = [];
  const mediumImpact = [];
  const explore = [];

  for (const f of findings) {
    if (f.category === 't1') highImpact.push(f);
    else if (f.category === 't2') mediumImpact.push(f);
    else explore.push(f);
  }

  return { highImpact, mediumImpact, explore };
}
