/**
 * SET Scanner — Settings.json Validator
 * Validates schema, detects unknown/deprecated keys, type mismatches.
 * Finding IDs: CA-SET-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 { extractKeys } from './lib/string-utils.mjs';

const SCANNER = 'SET';

/** Known top-level settings.json keys (as of April 2026) */
const KNOWN_KEYS = new Set([
  'additionalDirectories',
  'agent', 'allowedChannelPlugins', 'allowedHttpHookUrls', 'allowedMcpServers',
  'allowManagedHooksOnly', 'allowManagedMcpServersOnly', 'allowManagedPermissionRulesOnly',
  'alwaysThinkingEnabled', 'apiKeyHelper', 'attribution', 'autoMemoryDirectory',
  'autoMemoryEnabled', 'autoMode', 'autoUpdatesChannel', 'availableModels',
  'awsAuthRefresh', 'awsCredentialExport', 'blockedMarketplaces', 'channelsEnabled',
  'cleanupPeriodDays', 'claudeMdExcludes', 'companyAnnouncements', 'defaultShell',
  'deniedMcpServers', 'disableAllHooks', 'disableAutoMode', 'disableDeepLinkRegistration',
  'disabledMcpjsonServers', 'effortLevel', 'enableAllProjectMcpServers',
  'enabledMcpjsonServers', 'enabledPlugins', 'env', 'extraKnownMarketplaces',
  'fastModePerSessionOptIn', 'feedbackSurveyRate', 'fileSuggestion',
  'forceLoginMethod', 'forceLoginOrgUUID', 'hooks', 'httpHookAllowedEnvVars',
  'includeCoAuthoredBy', 'includeGitInstructions', 'language', 'model',
  'modelOverrides', 'otelHeadersHelper', 'outputStyle', 'permissions',
  'plansDirectory', 'pluginTrustMessage', 'prefersReducedMotion',
  'respectGitignore', 'showClearContextOnPlanAccept', 'showThinkingSummaries',
  'spinnerTipsEnabled', 'spinnerTipsOverride', 'spinnerVerbs', 'statusLine',
  'strictKnownMarketplaces', 'useAutoModeDuringPlan', 'voiceEnabled',
  'worktree', '$schema',
]);

/** Deprecated keys with migration info */
const DEPRECATED_KEYS = new Map([
  ['includeCoAuthoredBy', 'Use "attribution" instead'],
]);

/** Keys that require specific types */
const TYPE_CHECKS = new Map([
  ['alwaysThinkingEnabled', 'boolean'],
  ['autoMemoryEnabled', 'boolean'],
  ['channelsEnabled', 'boolean'],
  ['cleanupPeriodDays', 'number'],
  ['disableAllHooks', 'boolean'],
  ['effortLevel', 'string'],
  ['enableAllProjectMcpServers', 'boolean'],
  ['fastModePerSessionOptIn', 'boolean'],
  ['feedbackSurveyRate', 'number'],
  ['includeGitInstructions', 'boolean'],
  ['language', 'string'],
  ['model', 'string'],
  ['outputStyle', 'string'],
  ['prefersReducedMotion', 'boolean'],
  ['respectGitignore', 'boolean'],
  ['showThinkingSummaries', 'boolean'],
  ['spinnerTipsEnabled', 'boolean'],
  ['voiceEnabled', 'boolean'],
]);

/** Valid effortLevel values */
const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'max']);

/** v5 M6: warn when additionalDirectories grows beyond this — each entry adds
 * a project root to walks/discovery, inflating per-turn cost and confusing scope. */
const ADDITIONAL_DIRS_THRESHOLD = 2;

/**
 * Scan all settings.json files discovered.
 * @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 settingsFiles = discovery.files.filter(f => f.type === 'settings-json');
  const findings = [];
  let filesScanned = 0;

  if (settingsFiles.length === 0) {
    return scannerResult(SCANNER, 'skipped', [], 0, Date.now() - start);
  }

  for (const file of settingsFiles) {
    const content = await readTextFile(file.absPath);
    if (!content) continue;
    filesScanned++;

    const parsed = parseJson(content);
    if (parsed === null) {
      findings.push(finding({
        scanner: SCANNER,
        severity: SEVERITY.critical,
        title: 'Invalid JSON in settings file',
        description: `${file.relPath} contains invalid JSON and will be ignored by Claude Code.`,
        file: file.absPath,
        recommendation: 'Fix JSON syntax errors. Use a JSON validator.',
        autoFixable: false,
      }));
      continue;
    }

    // Check for unknown keys
    for (const key of Object.keys(parsed)) {
      if (!KNOWN_KEYS.has(key)) {
        findings.push(finding({
          scanner: SCANNER,
          severity: SEVERITY.medium,
          title: 'Unknown settings key',
          description: `${file.relPath}: "${key}" is not a recognized settings.json key. It will be silently ignored.`,
          file: file.absPath,
          evidence: key,
          recommendation: 'Check spelling. See https://json.schemastore.org/claude-code-settings.json for valid keys.',
          autoFixable: false,
        }));
      }
    }

    // Check for deprecated keys
    for (const [key, migration] of DEPRECATED_KEYS) {
      if (parsed[key] !== undefined) {
        findings.push(finding({
          scanner: SCANNER,
          severity: SEVERITY.medium,
          title: 'Deprecated settings key',
          description: `${file.relPath}: "${key}" is deprecated. ${migration}`,
          file: file.absPath,
          evidence: `${key}: ${JSON.stringify(parsed[key])}`,
          recommendation: migration,
          autoFixable: true,
        }));
      }
    }

    // Type validation
    for (const [key, expectedType] of TYPE_CHECKS) {
      if (parsed[key] !== undefined && typeof parsed[key] !== expectedType) {
        findings.push(finding({
          scanner: SCANNER,
          severity: SEVERITY.high,
          title: 'Type mismatch in settings',
          description: `${file.relPath}: "${key}" should be ${expectedType}, got ${typeof parsed[key]}.`,
          file: file.absPath,
          evidence: `${key}: ${JSON.stringify(parsed[key])} (${typeof parsed[key]})`,
          recommendation: `Change "${key}" to a ${expectedType} value.`,
          autoFixable: true,
        }));
      }
    }

    // effortLevel value check
    if (parsed.effortLevel && !VALID_EFFORT_LEVELS.has(parsed.effortLevel)) {
      findings.push(finding({
        scanner: SCANNER,
        severity: SEVERITY.medium,
        title: 'Invalid effortLevel value',
        description: `${file.relPath}: effortLevel "${parsed.effortLevel}" is not valid.`,
        file: file.absPath,
        evidence: `effortLevel: "${parsed.effortLevel}"`,
        recommendation: `Use one of: ${[...VALID_EFFORT_LEVELS].join(', ')}`,
        autoFixable: true,
      }));
    }

    // Missing $schema hint
    if (!parsed.$schema) {
      findings.push(finding({
        scanner: SCANNER,
        severity: SEVERITY.info,
        title: 'Missing $schema reference',
        description: `${file.relPath} lacks a $schema reference. Adding one enables autocomplete in VS Code/Cursor.`,
        file: file.absPath,
        recommendation: 'Add: "$schema": "https://json.schemastore.org/claude-code-settings.json"',
        autoFixable: true,
      }));
    }

    // Permissions checks
    if (parsed.permissions) {
      const perms = parsed.permissions;

      if (!perms.deny || (Array.isArray(perms.deny) && perms.deny.length === 0)) {
        findings.push(finding({
          scanner: SCANNER,
          severity: SEVERITY.medium,
          title: 'No deny rules configured',
          description: `${file.relPath}: No permission deny rules. Claude can access all files including .env and secrets.`,
          file: file.absPath,
          recommendation: 'Add deny rules for sensitive files: "deny": ["Read(./.env)", "Read(./secrets/**)"]',
          autoFixable: false,
        }));
      }

      if (!perms.allow || (Array.isArray(perms.allow) && perms.allow.length === 0)) {
        findings.push(finding({
          scanner: SCANNER,
          severity: SEVERITY.low,
          title: 'No allow rules configured',
          description: `${file.relPath}: No permission allow rules. This means frequent permission prompts for common operations.`,
          file: file.absPath,
          recommendation: 'Add allow rules for common tools: "allow": ["Bash(npm run *)", "Read(src/**)"]',
          autoFixable: false,
        }));
      }
    }

    // additionalDirectories threshold (v5 M6)
    if (Array.isArray(parsed.additionalDirectories) &&
        parsed.additionalDirectories.length > ADDITIONAL_DIRS_THRESHOLD) {
      findings.push(finding({
        scanner: SCANNER,
        severity: SEVERITY.low,
        title: 'Many additionalDirectories entries',
        description:
          `${file.relPath}: additionalDirectories has ${parsed.additionalDirectories.length} ` +
          `entries (>${ADDITIONAL_DIRS_THRESHOLD}). Each entry expands Claude's read scope ` +
          'across additional project roots, inflating discovery cost and risking unintended access.',
        file: file.absPath,
        evidence: parsed.additionalDirectories.slice(0, 5).map(d => `"${d}"`).join(', '),
        recommendation:
          'Trim to the minimum set needed. Prefer launching Claude from the relevant root ' +
          'rather than chaining many directories.',
        autoFixable: false,
      }));
    }

    // hooks checks (basic — detailed in hook-validator)
    if (parsed.hooks) {
      if (Array.isArray(parsed.hooks)) {
        findings.push(finding({
          scanner: SCANNER,
          severity: SEVERITY.critical,
          title: 'Hooks configured as array instead of object',
          description: `${file.relPath}: "hooks" must be an object with event keys, not an array. All hooks will be ignored.`,
          file: file.absPath,
          evidence: '"hooks": [...]',
          recommendation: 'Change to object format: "hooks": { "PreToolUse": [...] }',
          autoFixable: true,
        }));
      }
    }
  }

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