/**
 * Global configuration loader
 *
 * Manages ~/.takt/config.yaml.
 * GlobalConfigManager encapsulates the config cache as a singleton.
 */

import { readFileSync, existsSync, writeFileSync, statSync, accessSync, constants } from 'node:fs';
import { isAbsolute } from 'node:path';
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
import { GlobalConfigSchema } from '../../../core/models/index.js';
import type { Language } from '../../../core/models/index.js';
import type { PersistedGlobalConfig, PersonaProviderEntry } from '../../../core/models/persisted-global-config.js';
import {
  normalizeConfigProviderReference,
  type ConfigProviderReference,
} from '../providerReference.js';
import {
  normalizeProviderProfiles,
  denormalizeProviderProfiles,
  normalizePieceOverrides,
  denormalizePieceOverrides,
} from '../configNormalizers.js';
import { getGlobalConfigPath } from '../paths.js';
import { DEFAULT_LANGUAGE } from '../../../shared/constants.js';
import { parseProviderModel } from '../../../shared/utils/providerModel.js';
import { applyGlobalConfigEnvOverrides, envVarNameFromPath } from '../env/config-env-overrides.js';
import { invalidateAllResolvedConfigCache } from '../resolutionCache.js';

type ProviderType = NonNullable<PersistedGlobalConfig['provider']>;
type RawPersonaProviderBlock = {
  type: ProviderType;
  model?: string;
};
type RawProviderReference = ConfigProviderReference<ProviderType>;

/** Claude-specific model aliases that are not valid for other providers */
const CLAUDE_MODEL_ALIASES = new Set(['opus', 'sonnet', 'haiku']);

function hasControlCharacters(value: string): boolean {
  for (let index = 0; index < value.length; index++) {
    const code = value.charCodeAt(index);
    if (code < 32 || code === 127) {
      return true;
    }
  }
  return false;
}

/** Validate a CLI path value: must be non-empty, absolute, existing, executable file without control characters. */
export function validateCliPath(pathValue: string, sourceName: string): string {
  const trimmed = pathValue.trim();
  if (trimmed.length === 0) {
    throw new Error(`Configuration error: ${sourceName} must not be empty.`);
  }
  if (hasControlCharacters(trimmed)) {
    throw new Error(`Configuration error: ${sourceName} contains control characters.`);
  }
  if (!isAbsolute(trimmed)) {
    throw new Error(`Configuration error: ${sourceName} must be an absolute path: ${trimmed}`);
  }
  if (!existsSync(trimmed)) {
    throw new Error(`Configuration error: ${sourceName} path does not exist: ${trimmed}`);
  }
  const stats = statSync(trimmed);
  if (!stats.isFile()) {
    throw new Error(`Configuration error: ${sourceName} must point to an executable file: ${trimmed}`);
  }
  try {
    accessSync(trimmed, constants.X_OK);
  } catch {
    throw new Error(`Configuration error: ${sourceName} file is not executable: ${trimmed}`);
  }
  return trimmed;
}

function validateProviderModelCompatibility(provider: string | undefined, model: string | undefined): void {
  if (!provider) return;

  if (provider === 'opencode' && !model) {
    throw new Error(
      "Configuration error: provider 'opencode' requires model in 'provider/model' format (e.g. 'opencode/big-pickle')."
    );
  }

  if (!model) return;

  if ((provider === 'codex' || provider === 'opencode') && CLAUDE_MODEL_ALIASES.has(model)) {
    throw new Error(
      `Configuration error: model '${model}' is a Claude model alias but provider is '${provider}'. ` +
      `Either change the provider to 'claude' or specify a ${provider}-compatible model.`
    );
  }

  if (provider === 'opencode') {
    parseProviderModel(model, "Configuration error: model");
  }
}

function normalizePersonaProviders(
  raw: Record<string, ProviderType | PersonaProviderEntry | RawPersonaProviderBlock> | undefined,
): Record<string, PersonaProviderEntry> | undefined {
  if (!raw) return undefined;
  return Object.fromEntries(
    Object.entries(raw).map(([persona, entry]) => {
      let normalized: PersonaProviderEntry;
      if (typeof entry === 'string') {
        normalized = { provider: entry };
      } else if ('type' in entry) {
        normalized = { provider: entry.type, model: entry.model };
      } else {
        normalized = entry;
      }
      validateProviderModelCompatibility(normalized.provider, normalized.model);
      return [persona, normalized];
    }),
  );
}

/**
 * Manages global configuration loading and caching.
 * Singleton — use GlobalConfigManager.getInstance().
 */
export class GlobalConfigManager {
  private static instance: GlobalConfigManager | null = null;
  private cachedConfig: PersistedGlobalConfig | null = null;

  private constructor() {}

  static getInstance(): GlobalConfigManager {
    if (!GlobalConfigManager.instance) {
      GlobalConfigManager.instance = new GlobalConfigManager();
    }
    return GlobalConfigManager.instance;
  }

  /** Reset singleton for testing */
  static resetInstance(): void {
    GlobalConfigManager.instance = null;
  }

  /** Invalidate the cached configuration */
  invalidateCache(): void {
    this.cachedConfig = null;
  }

  /** Load global configuration (cached) */
  load(): PersistedGlobalConfig {
    if (this.cachedConfig !== null) {
      return this.cachedConfig;
    }
    const configPath = getGlobalConfigPath();

    const rawConfig: Record<string, unknown> = {};
    if (existsSync(configPath)) {
      const content = readFileSync(configPath, 'utf-8');
      const parsedRaw = parseYaml(content);
      if (parsedRaw && typeof parsedRaw === 'object' && !Array.isArray(parsedRaw)) {
        Object.assign(rawConfig, parsedRaw as Record<string, unknown>);
      } else if (parsedRaw != null) {
        throw new Error('Configuration error: ~/.takt/config.yaml must be a YAML object.');
      }
    }

    applyGlobalConfigEnvOverrides(rawConfig);

    const parsed = GlobalConfigSchema.parse(rawConfig);
    const normalizedProvider = normalizeConfigProviderReference(
      parsed.provider as RawProviderReference,
      parsed.model,
      parsed.provider_options as Record<string, unknown> | undefined,
    );
    const config: PersistedGlobalConfig = {
      language: parsed.language,
      logLevel: parsed.log_level,
      provider: normalizedProvider.provider,
      model: normalizedProvider.model,
      piece: parsed.piece,
      observability: parsed.observability ? {
        providerEvents: parsed.observability.provider_events,
      } : undefined,
      analytics: parsed.analytics ? {
        enabled: parsed.analytics.enabled,
        eventsPath: parsed.analytics.events_path,
        retentionDays: parsed.analytics.retention_days,
      } : undefined,
      worktreeDir: parsed.worktree_dir,
      autoPr: parsed.auto_pr,
      draftPr: parsed.draft_pr,
      disabledBuiltins: parsed.disabled_builtins,
      enableBuiltinPieces: parsed.enable_builtin_pieces,
      anthropicApiKey: parsed.anthropic_api_key,
      openaiApiKey: parsed.openai_api_key,
      codexCliPath: parsed.codex_cli_path,
      claudeCliPath: parsed.claude_cli_path,
      cursorCliPath: parsed.cursor_cli_path,
      opencodeApiKey: parsed.opencode_api_key,
      cursorApiKey: parsed.cursor_api_key,
      pipeline: parsed.pipeline ? {
        defaultBranchPrefix: parsed.pipeline.default_branch_prefix,
        commitMessageTemplate: parsed.pipeline.commit_message_template,
        prBodyTemplate: parsed.pipeline.pr_body_template,
      } : undefined,
      minimalOutput: parsed.minimal_output,
      bookmarksFile: parsed.bookmarks_file,
      pieceCategoriesFile: parsed.piece_categories_file,
      personaProviders: normalizePersonaProviders(
        parsed.persona_providers as Record<string, ProviderType | PersonaProviderEntry | RawPersonaProviderBlock> | undefined,
      ),
      providerOptions: normalizedProvider.providerOptions,
      providerProfiles: normalizeProviderProfiles(parsed.provider_profiles as Record<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | undefined),
      runtime: parsed.runtime?.prepare && parsed.runtime.prepare.length > 0
        ? { prepare: [...new Set(parsed.runtime.prepare)] }
        : undefined,
      branchNameStrategy: parsed.branch_name_strategy,
      preventSleep: parsed.prevent_sleep,
      notificationSound: parsed.notification_sound,
      notificationSoundEvents: parsed.notification_sound_events ? {
        iterationLimit: parsed.notification_sound_events.iteration_limit,
        pieceComplete: parsed.notification_sound_events.piece_complete,
        pieceAbort: parsed.notification_sound_events.piece_abort,
        runComplete: parsed.notification_sound_events.run_complete,
        runAbort: parsed.notification_sound_events.run_abort,
      } : undefined,
      interactivePreviewMovements: parsed.interactive_preview_movements,
      verbose: parsed.verbose,
      concurrency: parsed.concurrency,
      taskPollIntervalMs: parsed.task_poll_interval_ms,
      autoFetch: parsed.auto_fetch,
      baseBranch: parsed.base_branch,
      pieceOverrides: normalizePieceOverrides(parsed.piece_overrides as { quality_gates?: string[]; quality_gates_edit_only?: boolean; movements?: Record<string, { quality_gates?: string[] }> } | undefined),
    };
    validateProviderModelCompatibility(config.provider, config.model);
    this.cachedConfig = config;
    return config;
  }

  /** Save global configuration to disk and invalidate cache */
  save(config: PersistedGlobalConfig): void {
    const configPath = getGlobalConfigPath();
    const raw: Record<string, unknown> = {
      language: config.language,
      log_level: config.logLevel,
      provider: config.provider,
    };
    if (config.model) {
      raw.model = config.model;
    }
    if (config.piece) {
      raw.piece = config.piece;
    }
    if (config.observability && config.observability.providerEvents !== undefined) {
      raw.observability = {
        provider_events: config.observability.providerEvents,
      };
    }
    if (config.analytics) {
      const analyticsRaw: Record<string, unknown> = {};
      if (config.analytics.enabled !== undefined) analyticsRaw.enabled = config.analytics.enabled;
      if (config.analytics.eventsPath) analyticsRaw.events_path = config.analytics.eventsPath;
      if (config.analytics.retentionDays !== undefined) analyticsRaw.retention_days = config.analytics.retentionDays;
      if (Object.keys(analyticsRaw).length > 0) {
        raw.analytics = analyticsRaw;
      }
    }
    if (config.worktreeDir) {
      raw.worktree_dir = config.worktreeDir;
    }
    if (config.autoPr !== undefined) {
      raw.auto_pr = config.autoPr;
    }
    if (config.draftPr !== undefined) {
      raw.draft_pr = config.draftPr;
    }
    if (config.disabledBuiltins && config.disabledBuiltins.length > 0) {
      raw.disabled_builtins = config.disabledBuiltins;
    }
    if (config.enableBuiltinPieces !== undefined) {
      raw.enable_builtin_pieces = config.enableBuiltinPieces;
    }
    if (config.anthropicApiKey) {
      raw.anthropic_api_key = config.anthropicApiKey;
    }
    if (config.openaiApiKey) {
      raw.openai_api_key = config.openaiApiKey;
    }
    if (config.codexCliPath) {
      raw.codex_cli_path = config.codexCliPath;
    }
    if (config.claudeCliPath) {
      raw.claude_cli_path = config.claudeCliPath;
    }
    if (config.cursorCliPath) {
      raw.cursor_cli_path = config.cursorCliPath;
    }
    if (config.opencodeApiKey) {
      raw.opencode_api_key = config.opencodeApiKey;
    }
    if (config.cursorApiKey) {
      raw.cursor_api_key = config.cursorApiKey;
    }
    if (config.pipeline) {
      const pipelineRaw: Record<string, unknown> = {};
      if (config.pipeline.defaultBranchPrefix) pipelineRaw.default_branch_prefix = config.pipeline.defaultBranchPrefix;
      if (config.pipeline.commitMessageTemplate) pipelineRaw.commit_message_template = config.pipeline.commitMessageTemplate;
      if (config.pipeline.prBodyTemplate) pipelineRaw.pr_body_template = config.pipeline.prBodyTemplate;
      if (Object.keys(pipelineRaw).length > 0) {
        raw.pipeline = pipelineRaw;
      }
    }
    if (config.minimalOutput !== undefined) {
      raw.minimal_output = config.minimalOutput;
    }
    if (config.bookmarksFile) {
      raw.bookmarks_file = config.bookmarksFile;
    }
    if (config.pieceCategoriesFile) {
      raw.piece_categories_file = config.pieceCategoriesFile;
    }
    if (config.personaProviders && Object.keys(config.personaProviders).length > 0) {
      raw.persona_providers = config.personaProviders;
    }
    const rawProviderProfiles = denormalizeProviderProfiles(config.providerProfiles);
    if (rawProviderProfiles && Object.keys(rawProviderProfiles).length > 0) {
      raw.provider_profiles = rawProviderProfiles;
    }
    if (config.runtime?.prepare && config.runtime.prepare.length > 0) {
      raw.runtime = {
        prepare: [...new Set(config.runtime.prepare)],
      };
    }
    if (config.branchNameStrategy) {
      raw.branch_name_strategy = config.branchNameStrategy;
    }
    if (config.preventSleep !== undefined) {
      raw.prevent_sleep = config.preventSleep;
    }
    if (config.notificationSound !== undefined) {
      raw.notification_sound = config.notificationSound;
    }
    if (config.notificationSoundEvents) {
      const eventRaw: Record<string, unknown> = {};
      if (config.notificationSoundEvents.iterationLimit !== undefined) {
        eventRaw.iteration_limit = config.notificationSoundEvents.iterationLimit;
      }
      if (config.notificationSoundEvents.pieceComplete !== undefined) {
        eventRaw.piece_complete = config.notificationSoundEvents.pieceComplete;
      }
      if (config.notificationSoundEvents.pieceAbort !== undefined) {
        eventRaw.piece_abort = config.notificationSoundEvents.pieceAbort;
      }
      if (config.notificationSoundEvents.runComplete !== undefined) {
        eventRaw.run_complete = config.notificationSoundEvents.runComplete;
      }
      if (config.notificationSoundEvents.runAbort !== undefined) {
        eventRaw.run_abort = config.notificationSoundEvents.runAbort;
      }
      if (Object.keys(eventRaw).length > 0) {
        raw.notification_sound_events = eventRaw;
      }
    }
    if (config.interactivePreviewMovements !== undefined) {
      raw.interactive_preview_movements = config.interactivePreviewMovements;
    }
    if (config.verbose) {
      raw.verbose = config.verbose;
    }
    if (config.concurrency !== undefined && config.concurrency > 1) {
      raw.concurrency = config.concurrency;
    }
    if (config.taskPollIntervalMs !== undefined && config.taskPollIntervalMs !== 500) {
      raw.task_poll_interval_ms = config.taskPollIntervalMs;
    }
    if (config.autoFetch) {
      raw.auto_fetch = config.autoFetch;
    }
    if (config.baseBranch) {
      raw.base_branch = config.baseBranch;
    }
    const denormalizedPieceOverrides = denormalizePieceOverrides(config.pieceOverrides);
    if (denormalizedPieceOverrides) {
      raw.piece_overrides = denormalizedPieceOverrides;
    }
    writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
    this.invalidateCache();
    invalidateAllResolvedConfigCache();
  }
}

export function invalidateGlobalConfigCache(): void {
  GlobalConfigManager.getInstance().invalidateCache();
  invalidateAllResolvedConfigCache();
}

export function loadGlobalConfig(): PersistedGlobalConfig {
  return GlobalConfigManager.getInstance().load();
}

export function saveGlobalConfig(config: PersistedGlobalConfig): void {
  GlobalConfigManager.getInstance().save(config);
}

export function getDisabledBuiltins(): string[] {
  try {
    const config = loadGlobalConfig();
    return config.disabledBuiltins ?? [];
  } catch {
    return [];
  }
}

export function getBuiltinPiecesEnabled(): boolean {
  try {
    const config = loadGlobalConfig();
    return config.enableBuiltinPieces !== false;
  } catch {
    return true;
  }
}

export function getLanguage(): Language {
  try {
    const config = loadGlobalConfig();
    return config.language;
  } catch {
    return DEFAULT_LANGUAGE;
  }
}

export function setLanguage(language: Language): void {
  const config = loadGlobalConfig();
  config.language = language;
  saveGlobalConfig(config);
}

export function setProvider(provider: 'claude' | 'codex' | 'opencode' | 'cursor' | 'copilot'): void {
  const config = loadGlobalConfig();
  config.provider = provider;
  saveGlobalConfig(config);
}

/**
 * Resolve the Anthropic API key.
 * Priority: TAKT_ANTHROPIC_API_KEY env var > config.yaml > undefined (CLI auth fallback)
 */
export function resolveAnthropicApiKey(): string | undefined {
  const envKey = process.env[envVarNameFromPath('anthropic_api_key')];
  if (envKey) return envKey;

  try {
    const config = loadGlobalConfig();
    return config.anthropicApiKey;
  } catch {
    return undefined;
  }
}

/**
 * Resolve the OpenAI API key.
 * Priority: TAKT_OPENAI_API_KEY env var > config.yaml > undefined (CLI auth fallback)
 */
export function resolveOpenaiApiKey(): string | undefined {
  const envKey = process.env[envVarNameFromPath('openai_api_key')];
  if (envKey) return envKey;

  try {
    const config = loadGlobalConfig();
    return config.openaiApiKey;
  } catch {
    return undefined;
  }
}

/**
 * Resolve the Codex CLI path override.
 * Priority: TAKT_CODEX_CLI_PATH env var > config.yaml > undefined (SDK vendored binary fallback)
 */
export function resolveCodexCliPath(projectConfig?: { codexCliPath?: string }): string | undefined {
  const envPath = process.env[envVarNameFromPath('codex_cli_path')];
  if (envPath !== undefined) {
    return validateCliPath(envPath, 'TAKT_CODEX_CLI_PATH');
  }

  if (projectConfig?.codexCliPath !== undefined) {
    return validateCliPath(projectConfig.codexCliPath, 'codex_cli_path (project)');
  }

  let config: PersistedGlobalConfig;
  try {
    config = loadGlobalConfig();
  } catch {
    return undefined;
  }
  if (config.codexCliPath === undefined) {
    return undefined;
  }
  return validateCliPath(config.codexCliPath, 'codex_cli_path');
}

/**
 * Resolve the Claude Code CLI path override.
 * Priority: TAKT_CLAUDE_CLI_PATH env var > project config > global config > undefined (SDK default)
 */
export function resolveClaudeCliPath(projectConfig?: { claudeCliPath?: string }): string | undefined {
  const envPath = process.env[envVarNameFromPath('claude_cli_path')];
  if (envPath !== undefined) {
    return validateCliPath(envPath, 'TAKT_CLAUDE_CLI_PATH');
  }

  if (projectConfig?.claudeCliPath !== undefined) {
    return validateCliPath(projectConfig.claudeCliPath, 'claude_cli_path (project)');
  }

  let config: PersistedGlobalConfig;
  try {
    config = loadGlobalConfig();
  } catch {
    return undefined;
  }
  if (config.claudeCliPath === undefined) {
    return undefined;
  }
  return validateCliPath(config.claudeCliPath, 'claude_cli_path');
}

/**
 * Resolve the cursor-agent CLI path override.
 * Priority: TAKT_CURSOR_CLI_PATH env var > project config > global config > undefined (default 'cursor-agent')
 */
export function resolveCursorCliPath(projectConfig?: { cursorCliPath?: string }): string | undefined {
  const envPath = process.env[envVarNameFromPath('cursor_cli_path')];
  if (envPath !== undefined) {
    return validateCliPath(envPath, 'TAKT_CURSOR_CLI_PATH');
  }

  if (projectConfig?.cursorCliPath !== undefined) {
    return validateCliPath(projectConfig.cursorCliPath, 'cursor_cli_path (project)');
  }

  let config: PersistedGlobalConfig;
  try {
    config = loadGlobalConfig();
  } catch {
    return undefined;
  }
  if (config.cursorCliPath === undefined) {
    return undefined;
  }
  return validateCliPath(config.cursorCliPath, 'cursor_cli_path');
}

/**
 * Resolve the OpenCode API key.
 * Priority: TAKT_OPENCODE_API_KEY env var > config.yaml > undefined
 */
export function resolveOpencodeApiKey(): string | undefined {
  const envKey = process.env[envVarNameFromPath('opencode_api_key')];
  if (envKey) return envKey;

  try {
    const config = loadGlobalConfig();
    return config.opencodeApiKey;
  } catch {
    return undefined;
  }
}

/**
 * Resolve the Cursor API key.
 * Priority: TAKT_CURSOR_API_KEY env var > config.yaml > undefined (cursor-agent login fallback)
 */
export function resolveCursorApiKey(): string | undefined {
  const envKey = process.env[envVarNameFromPath('cursor_api_key')];
  if (envKey) return envKey;

  try {
    const config = loadGlobalConfig();
    return config.cursorApiKey;
  } catch {
    return undefined;
  }
}

/**
 * Resolve the Copilot CLI path override.
 * Priority: TAKT_COPILOT_CLI_PATH env var > project config > global config > undefined (default 'copilot')
 */
export function resolveCopilotCliPath(projectConfig?: { copilotCliPath?: string }): string | undefined {
  const envPath = process.env[envVarNameFromPath('copilot_cli_path')];
  if (envPath !== undefined) {
    return validateCliPath(envPath, 'TAKT_COPILOT_CLI_PATH');
  }

  if (projectConfig?.copilotCliPath !== undefined) {
    return validateCliPath(projectConfig.copilotCliPath, 'copilot_cli_path (project)');
  }

  let config: PersistedGlobalConfig;
  try {
    config = loadGlobalConfig();
  } catch {
    return undefined;
  }
  if (config.copilotCliPath === undefined) {
    return undefined;
  }
  return validateCliPath(config.copilotCliPath, 'copilot_cli_path');
}

/**
 * Resolve the Copilot GitHub token.
 * Priority: TAKT_COPILOT_GITHUB_TOKEN env var > config.yaml > undefined
 */
export function resolveCopilotGithubToken(): string | undefined {
  const envKey = process.env[envVarNameFromPath('copilot_github_token')];
  if (envKey) return envKey;

  try {
    const config = loadGlobalConfig();
    return config.copilotGithubToken;
  } catch {
    return undefined;
  }
}
