'use strict';

const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

const { writeFileAtomic } = require('./atomic-write');
const {
  hasExplicitCommitAttributionPreference,
  withCommitAttributionDisabled,
} = require('./claude-commit-attribution');
const { normalizeGitHubGitOrigin } = require('./github-origin');
const {
  CURRENT_PLUGIN_ID,
  LEGACY_PLUGIN_IDS,
  findManagedClaudeInstalls,
  findManualClaudePlugin,
  resolveClaudePaths,
} = require('./install/inventory');

const OFFICIAL_MARKETPLACE_NAME = 'ecc';
const OFFICIAL_MARKETPLACE_REPO = 'affaan-m/ecc';
const OFFICIAL_MARKETPLACE_URL = 'https://github.com/affaan-m/ECC';
const PROVIDER_COMMAND_TIMEOUT_MS = 120 * 1000;
const VALID_SCOPES = new Set(['user', 'project', 'local']);
const VALID_HOOK_MODES = new Set(['off', 'minimal', 'standard', 'strict']);

class ClaudeSetupError extends Error {
  constructor(code, message, details = {}) {
    super(message);
    this.name = 'ClaudeSetupError';
    this.code = code;
    this.phase = details.phase || 'preflight';
    this.observedScopes = [...(details.observedScopes || [])];
    this.recovery = [...(details.recovery || [])];
  }

  toJSON() {
    return {
      error: {
        code: this.code,
        message: this.message,
        phase: this.phase,
        observedScopes: [...this.observedScopes],
        recovery: [...this.recovery],
      },
    };
  }
}

function fail(code, message, details) {
  throw new ClaudeSetupError(code, message, details);
}

function normalizeGitHubRepository(value) {
  if (typeof value !== 'string') return null;
  const normalized = value.trim().replace(/\.git$/i, '').replace(/\/+$/, '');
  const match = normalized.match(/^([^/]+\/[^/]+)$/);
  return match ? match[1].toLowerCase() : null;
}

function normalizeMarketplaceRepository(marketplace) {
  return marketplace?.source === 'github'
    ? normalizeGitHubRepository(marketplace.repo)
    : normalizeGitHubGitOrigin(marketplace?.url);
}

function isOfficialMarketplace(marketplace) {
  if (!marketplace || marketplace.name !== OFFICIAL_MARKETPLACE_NAME) return false;
  return normalizeMarketplaceRepository(marketplace) === OFFICIAL_MARKETPLACE_REPO;
}

function parseJsonArray(stdout, label) {
  let parsed;
  try {
    parsed = JSON.parse(String(stdout || ''));
  } catch (error) {
    fail(
      `INVALID_${label.toUpperCase()}_INVENTORY`,
      `Claude ${label} inventory returned invalid JSON: ${error.message}`
    );
  }
  if (!Array.isArray(parsed)) {
    fail(
      `INVALID_${label.toUpperCase()}_INVENTORY`,
      `Claude ${label} inventory is invalid: expected a JSON array`
    );
  }
  return parsed;
}

function parsePluginList(stdout) {
  const plugins = parseJsonArray(stdout, 'plugin');
  for (const plugin of plugins) {
    const isRelevant = plugin && (
      plugin.id === CURRENT_PLUGIN_ID
      || String(plugin.id || '').startsWith('ecc@')
      || LEGACY_PLUGIN_IDS.has(plugin.id)
      || String(plugin.id || '').startsWith('everything-claude-code@')
    );
    if (!isRelevant) continue;
    if (
      typeof plugin.id !== 'string'
      || !VALID_SCOPES.has(plugin.scope)
      || typeof plugin.enabled !== 'boolean'
    ) {
      fail(
        'INVALID_PLUGIN_INVENTORY',
        'Claude plugin inventory contains an invalid ECC plugin entry'
      );
    }
  }
  return plugins;
}

function parseMarketplaceList(stdout) {
  const marketplaces = parseJsonArray(stdout, 'marketplace');
  for (const marketplace of marketplaces) {
    if (!marketplace || marketplace.name !== OFFICIAL_MARKETPLACE_NAME) continue;
    if (
      typeof marketplace.name !== 'string'
      || typeof marketplace.source !== 'string'
      || !['github', 'git'].includes(marketplace.source)
      || !normalizeMarketplaceRepository(marketplace)
    ) {
      fail(
        'INVALID_MARKETPLACE_INVENTORY',
        'Claude marketplace inventory contains an invalid `ecc` entry'
      );
    }
  }
  return marketplaces;
}

const UNSAFE_WINDOWS_SHELL_CHARS = /[\r\n&|<>^%!]/;

function quoteWindowsCommandToken(value) {
  const token = String(value);
  if (UNSAFE_WINDOWS_SHELL_CHARS.test(token)) {
    throw new Error('Claude Code command contains characters that are unsafe for cmd.exe');
  }
  if (token === '') return '""';
  if (!/[\s"]/.test(token)) return token;
  return `"${token.replace(/"/g, '""')}"`;
}

function buildWindowsCommandLine(command, args) {
  return [command, ...args].map(quoteWindowsCommandToken).join(' ');
}

function resolveWindowsCmdShim(command, env) {
  if (typeof command !== 'string' || command.length === 0) return null;
  if (/\.(cmd|bat)$/i.test(command)) return command;
  if (path.extname(command)) return null;

  const isPathLike = path.isAbsolute(command)
    || command.includes('/')
    || command.includes('\\');
  if (isPathLike) {
    const candidate = `${command}.cmd`;
    return fs.existsSync(candidate) ? candidate : null;
  }

  const lookup = spawnSync('where.exe', [`${command}.cmd`], {
    env,
    encoding: 'utf8',
    windowsHide: true,
  });
  if (lookup.error || lookup.status !== 0) return null;
  return String(lookup.stdout || '')
    .split(/\r?\n/)
    .map(line => line.trim())
    .find(Boolean) || null;
}

function runClaude(args, options = {}, dependencies = {}) {
  const command = options.command || 'claude';
  const spawn = dependencies.spawnSync || spawnSync;
  const timeoutMs = options.timeoutMs ?? PROVIDER_COMMAND_TIMEOUT_MS;
  const spawnOptions = {
    cwd: options.cwd || process.cwd(),
    env: options.env || process.env,
    encoding: 'utf8',
    maxBuffer: 10 * 1024 * 1024,
    killSignal: 'SIGKILL',
    timeout: timeoutMs,
    windowsHide: true,
  };
  let result = spawn(command, args, spawnOptions);

  if (process.platform === 'win32' && result.error) {
    const shim = resolveWindowsCmdShim(command, spawnOptions.env);
    if (shim) {
      let commandLine;
      try {
        commandLine = buildWindowsCommandLine(shim, args);
      } catch (error) {
        fail(
          'CLAUDE_COMMAND_FAILED',
          `Could not run Claude Code: ${error.message}`,
          { phase: options.phase || 'provider' }
        );
      }
      result = spawn(commandLine, {
        ...spawnOptions,
        shell: true,
      });
    }
  }

  const timedOut = (
    result.error?.code === 'ETIMEDOUT'
    || (result.error?.killed === true && result.error?.signal === spawnOptions.killSignal)
  );
  if (timedOut) {
    fail(
      'CLAUDE_COMMAND_FAILED',
      `Claude Code command timed out after ${timeoutMs} ms`,
      { phase: options.phase || 'provider' }
    );
  }
  if (result.error) {
    if (result.error.code === 'ENOENT') {
      fail(
        'CLAUDE_NOT_FOUND',
        'Claude Code is not installed or `claude` is not on PATH. Install Claude Code, then rerun ECC setup.',
        { phase: options.phase || 'inventory' }
      );
    }
    fail(
      'CLAUDE_COMMAND_FAILED',
      `Could not run Claude Code: ${result.error.message}`,
      { phase: options.phase || 'provider' }
    );
  }
  if (result.status !== 0) {
    const detail = String(result.stderr || result.stdout || '').trim();
    fail(
      'CLAUDE_COMMAND_FAILED',
      `Claude Code command failed${detail ? `: ${detail}` : ''}`,
      { phase: options.phase || 'provider' }
    );
  }
  return result;
}

function readSettings(settingsPath) {
  if (!fs.existsSync(settingsPath)) return {};
  let settings;
  try {
    settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
  } catch (error) {
    fail(
      'INVALID_CLAUDE_SETTINGS',
      `Claude user settings are invalid at ${settingsPath}: ${error.message}`,
      { phase: 'preflight' }
    );
  }
  if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
    fail(
      'INVALID_CLAUDE_SETTINGS',
      `Claude user settings are invalid at ${settingsPath}: expected a JSON object`,
      { phase: 'preflight' }
    );
  }
  const pluginConfigs = settings.pluginConfigs;
  if (pluginConfigs !== undefined && (
    !pluginConfigs
    || typeof pluginConfigs !== 'object'
    || Array.isArray(pluginConfigs)
  )) {
    fail(
      'INVALID_CLAUDE_SETTINGS',
      `Claude user settings are invalid at ${settingsPath}: pluginConfigs must be an object`,
      { phase: 'preflight' }
    );
  }
  const eccConfig = pluginConfigs?.[CURRENT_PLUGIN_ID];
  if (eccConfig !== undefined && (
    !eccConfig
    || typeof eccConfig !== 'object'
    || Array.isArray(eccConfig)
  )) {
    fail(
      'INVALID_CLAUDE_SETTINGS',
      `Claude user settings are invalid at ${settingsPath}: ${CURRENT_PLUGIN_ID} config must be an object`,
      { phase: 'preflight' }
    );
  }
  if (eccConfig?.options !== undefined && (
    !eccConfig.options
    || typeof eccConfig.options !== 'object'
    || Array.isArray(eccConfig.options)
  )) {
    fail(
      'INVALID_CLAUDE_SETTINGS',
      `Claude user settings are invalid at ${settingsPath}: ${CURRENT_PLUGIN_ID} options must be an object`,
      { phase: 'preflight' }
    );
  }
  return settings;
}

function hookOptions(hooks) {
  return {
    hooks_enabled: hooks !== 'off',
    hook_profile: hooks === 'off' ? 'standard' : hooks,
  };
}

function readStoredHookOptions(settings) {
  const options = settings.pluginConfigs?.[CURRENT_PLUGIN_ID]?.options || {};
  return {
    hooks_enabled: options.hooks_enabled !== false,
    hook_profile: VALID_HOOK_MODES.has(options.hook_profile)
      && options.hook_profile !== 'off'
      ? options.hook_profile
      : 'standard',
  };
}

function deriveHookMode(settings) {
  const options = readStoredHookOptions(settings);
  return options.hooks_enabled ? options.hook_profile : 'off';
}

function withClaudeCommitAttributionPreference(settings) {
  return withCommitAttributionDisabled(settings);
}

function needsClaudeCommitAttributionPreferenceWrite(settings) {
  return !hasExplicitCommitAttributionPreference(settings);
}

function writeClaudePluginOptions(settingsPath, hooks) {
  const settings = readSettings(settingsPath);
  const pluginConfigs = settings.pluginConfigs || {};
  const eccConfig = pluginConfigs[CURRENT_PLUGIN_ID] || {};
  const options = eccConfig.options || {};
  const nextOptions = hooks === undefined
    ? { ...options }
    : {
      ...options,
      ...hookOptions(hooks),
    };
  const nextSettings = {
    ...withClaudeCommitAttributionPreference(settings),
    pluginConfigs: {
      ...pluginConfigs,
      [CURRENT_PLUGIN_ID]: {
        ...eccConfig,
        options: nextOptions,
      },
    },
  };
  writeFileAtomic(settingsPath, `${JSON.stringify(nextSettings, null, 2)}\n`);
  return settingsPath;
}

function currentEccPlugins(plugins) {
  return plugins.filter(plugin => plugin?.id === CURRENT_PLUGIN_ID);
}

function assertNoConflictingEccPlugins(plugins) {
  const legacy = plugins.find(plugin => (
    LEGACY_PLUGIN_IDS.has(plugin?.id)
    || String(plugin?.id || '').startsWith('everything-claude-code@')
  ));
  if (legacy) {
    fail(
      'LEGACY_PLUGIN_INSTALLED',
      `Legacy plugin ${legacy.id} is installed. Uninstall it before setting up ${CURRENT_PLUGIN_ID}.`,
      {
        observedScopes: [legacy.scope],
        recovery: [`claude plugin uninstall ${legacy.id} --scope ${legacy.scope} --keep-data`],
      }
    );
  }

  const conflictingEcc = plugins.find(plugin => (
    typeof plugin?.id === 'string'
    && plugin.id.startsWith('ecc@')
    && plugin.id !== CURRENT_PLUGIN_ID
  ));
  if (conflictingEcc) {
    fail(
      'DUPLICATE_ECC_PLUGIN',
      `${conflictingEcc.id} is already installed and would duplicate ECC surfaces. Uninstall it before setting up ${CURRENT_PLUGIN_ID}.`,
      {
        observedScopes: [conflictingEcc.scope],
        recovery: [
          `claude plugin uninstall ${conflictingEcc.id} --scope ${conflictingEcc.scope} --keep-data`,
        ],
      }
    );
  }
}

function inspectPluginInventory(plugins, requestedScope) {
  assertNoConflictingEccPlugins(plugins);
  const installed = currentEccPlugins(plugins);
  const observedScopes = installed.map(plugin => plugin.scope);
  if (installed.length > 1 || new Set(observedScopes).size !== observedScopes.length) {
    fail(
      'MULTIPLE_PLUGIN_SCOPES',
      `${CURRENT_PLUGIN_ID} is installed in multiple scopes. Resolve the duplicate scopes before setup.`,
      { observedScopes }
    );
  }

  if (!requestedScope && installed.length === 0) {
    fail(
      'SCOPE_REQUIRED',
      'A fresh install requires --scope user, project, or local.'
    );
  }

  const scope = requestedScope || installed[0].scope;
  if (!VALID_SCOPES.has(scope)) {
    fail('INVALID_SCOPE', `Invalid plugin scope: ${scope}`);
  }
  if (installed.length === 1 && installed[0].scope !== scope) {
    fail(
      'SCOPE_MOVE_REQUIRED',
      `${CURRENT_PLUGIN_ID} is already installed at ${installed[0].scope} scope. Use the scope migration workflow to move it to ${scope}.`,
      {
        observedScopes,
        recovery: [
          `ecc setup --mode claude-plugin --scope ${scope} --move-scope --yes`,
        ],
      }
    );
  }

  return {
    installed: installed[0] || null,
    observedScopes,
    scope,
  };
}

function assertSafeLocalInventory(options) {
  const manual = findManualClaudePlugin(options);
  if (manual) {
    fail(
      'MANUAL_PLUGIN_INSTALL',
      `A manual ECC plugin layout exists at ${manual.manifestPath}. Remove or migrate the manual install before setup.`
    );
  }
  let managedInstalls;
  try {
    managedInstalls = findManagedClaudeInstalls(options);
  } catch (error) {
    fail('INVALID_MANAGED_STATE', error.message);
  }
  const overlap = managedInstalls.find(install => install.overlapsPlugin);
  if (overlap) {
    fail(
      'MANAGED_INSTALL_OVERLAP',
      `Managed ECC content at ${overlap.statePath} overlaps the Claude plugin. Remove that managed overlap before setup.`
    );
  }
  return managedInstalls;
}

function ensureOfficialMarketplace(options) {
  const run = options.run || runClaude;
  const existing = options.marketplaces.find(entry => entry?.name === OFFICIAL_MARKETPLACE_NAME);
  if (existing && !isOfficialMarketplace(existing)) {
    fail(
      'MARKETPLACE_COLLISION',
      'Refusing the `ecc` marketplace collision because it is not the official affaan-m/ECC source.'
    );
  }

  if (existing) {
    run(
      ['plugin', 'marketplace', 'update', OFFICIAL_MARKETPLACE_NAME],
      { cwd: options.projectRoot, phase: 'marketplace' }
    );
  } else {
    run(
      [
        'plugin', 'marketplace', 'add',
        OFFICIAL_MARKETPLACE_URL,
        '--scope', options.scope,
      ],
      { cwd: options.projectRoot, phase: 'marketplace' }
    );
  }

  const verified = parseMarketplaceList(
    run(
      ['plugin', 'marketplace', 'list', '--json'],
      { cwd: options.projectRoot, phase: 'marketplace-verification' }
    ).stdout
  ).find(entry => entry?.name === OFFICIAL_MARKETPLACE_NAME);
  if (!verified || !isOfficialMarketplace(verified)) {
    fail(
      'MARKETPLACE_VERIFICATION_FAILED',
      'Could not verify the official ECC marketplace after the marketplace change.',
      { phase: 'marketplace-verification' }
    );
  }
  return verified;
}

function verifyPluginAtScope(options) {
  const run = options.run || runClaude;
  const plugins = parsePluginList(
    run(
      ['plugin', 'list', '--json'],
      { cwd: options.projectRoot, phase: options.phase || 'plugin-verification' }
    ).stdout
  );
  const installed = currentEccPlugins(plugins);
  const valid = (
    installed.length === 1
    && installed[0].scope === options.scope
    && installed[0].enabled === true
  );
  if (!valid) {
    fail(
      'PLUGIN_VERIFICATION_FAILED',
      `Could not verify ${CURRENT_PLUGIN_ID} as enabled only at ${options.scope} scope.`,
      {
        phase: options.phase || 'plugin-verification',
        observedScopes: installed.map(plugin => plugin.scope),
      }
    );
  }
  return installed[0];
}

function ensurePluginAtScope(options) {
  const run = options.run || runClaude;
  const configuredHooks = options.hookConfiguration || hookOptions(options.hooks);
  if (options.installed) {
    run(
      ['plugin', 'update', CURRENT_PLUGIN_ID, '--scope', options.scope],
      { cwd: options.projectRoot, phase: 'plugin-update' }
    );
    return 'updated';
  }
  run(
    [
      'plugin', 'install', CURRENT_PLUGIN_ID,
      '--scope', options.scope,
      '--config', `hooks_enabled=${configuredHooks.hooks_enabled}`,
      '--config', `hook_profile=${configuredHooks.hook_profile}`,
    ],
    { cwd: options.projectRoot, phase: 'plugin-install' }
  );
  return 'installed';
}

function setupClaudePlugin(options = {}, dependencies = {}) {
  const paths = resolveClaudePaths(options);
  if (options.hooks !== undefined && !VALID_HOOK_MODES.has(options.hooks)) {
    fail('INVALID_HOOK_MODE', `Invalid hook mode: ${options.hooks}`);
  }
  if (options.scope !== undefined && !VALID_SCOPES.has(options.scope)) {
    fail('INVALID_SCOPE', `Invalid plugin scope: ${options.scope}`);
  }

  const settingsPath = path.join(paths.configDir, 'settings.json');
  const initialSettings = readSettings(settingsPath);
  assertSafeLocalInventory(paths);

  const run = dependencies.runClaude || runClaude;
  const plugins = parsePluginList(
    run(
      ['plugin', 'list', '--json'],
      { cwd: paths.projectRoot, phase: 'inventory' }
    ).stdout
  );
  const inventory = inspectPluginInventory(plugins, options.scope);
  const hooks = options.hooks === undefined && inventory.installed
    ? deriveHookMode(initialSettings)
    : (options.hooks || 'standard');
  const marketplaces = parseMarketplaceList(
    run(
      ['plugin', 'marketplace', 'list', '--json'],
      { cwd: paths.projectRoot, phase: 'marketplace-inventory' }
    ).stdout
  );
  const namedMarketplace = marketplaces.find(entry => (
    entry?.name === OFFICIAL_MARKETPLACE_NAME
  ));
  if (namedMarketplace && !isOfficialMarketplace(namedMarketplace)) {
    fail(
      'MARKETPLACE_COLLISION',
      'Refusing the `ecc` marketplace collision because it is not the official affaan-m/ECC source.'
    );
  }

  if (options.dryRun) {
    return {
      action: inventory.installed ? 'would-update' : 'would-install',
      dryRun: true,
      hooks,
      marketplaceAction: namedMarketplace ? 'would-update' : 'would-add',
      pluginId: CURRENT_PLUGIN_ID,
      scope: inventory.scope,
    };
  }

  ensureOfficialMarketplace({
    marketplaces,
    projectRoot: paths.projectRoot,
    run,
    scope: inventory.scope,
  });
  const action = ensurePluginAtScope({
    hooks,
    installed: inventory.installed,
    projectRoot: paths.projectRoot,
    run,
    scope: inventory.scope,
  });
  verifyPluginAtScope({
    phase: 'plugin-verification',
    projectRoot: paths.projectRoot,
    run,
    scope: inventory.scope,
  });
  const hooksToPersist = options.hooks !== undefined || !inventory.installed
    ? hooks
    : undefined;
  if (
    options.hooks !== undefined
    || !inventory.installed
    || needsClaudeCommitAttributionPreferenceWrite(initialSettings)
  ) {
    writeClaudePluginOptions(settingsPath, hooksToPersist);
  }

  return {
    action,
    hooks,
    pluginId: CURRENT_PLUGIN_ID,
    restartRequired: true,
    scope: inventory.scope,
    settingsPath,
  };
}

module.exports = {
  ClaudeSetupError,
  CURRENT_PLUGIN_ID,
  OFFICIAL_MARKETPLACE_NAME,
  OFFICIAL_MARKETPLACE_URL,
  PROVIDER_COMMAND_TIMEOUT_MS,
  VALID_HOOK_MODES,
  VALID_SCOPES,
  buildWindowsCommandLine,
  assertNoConflictingEccPlugins,
  assertSafeLocalInventory,
  currentEccPlugins,
  deriveHookMode,
  ensureOfficialMarketplace,
  ensurePluginAtScope,
  hookOptions,
  inspectPluginInventory,
  isOfficialMarketplace,
  parseMarketplaceList,
  parsePluginList,
  readStoredHookOptions,
  readSettings,
  runClaude,
  setupClaudePlugin,
  verifyPluginAtScope,
  needsClaudeCommitAttributionPreferenceWrite,
  withClaudeCommitAttributionPreference,
  writeClaudePluginOptions,
};
