/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { AyuDark } from './builtin/dark/ayu-dark.js';
import { AyuLight } from './builtin/light/ayu-light.js';
import { AtomOneDark } from './builtin/dark/atom-one-dark.js';
import { Dracula } from './builtin/dark/dracula-dark.js';
import { GitHubDark } from './builtin/dark/github-dark.js';
import { GitHubLight } from './builtin/light/github-light.js';
import { GitHubDarkColorblind } from './builtin/dark/github-dark-colorblind.js';
import { GitHubLightColorblind } from './builtin/light/github-light-colorblind.js';
import { GoogleCode } from './builtin/light/googlecode-light.js';
import { Holiday } from './builtin/dark/holiday-dark.js';
import { DefaultLight } from './builtin/light/default-light.js';
import { DefaultDark } from './builtin/dark/default-dark.js';
import { ShadesOfPurple } from './builtin/dark/shades-of-purple-dark.js';
import { SolarizedDark } from './builtin/dark/solarized-dark.js';
import { SolarizedLight } from './builtin/light/solarized-light.js';
import { XCode } from './builtin/light/xcode-light.js';
import { TokyoNight } from './builtin/dark/tokyonight-dark.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import type { Theme, ThemeType, ColorsTheme, CustomTheme } from './theme.js';
import {
  createCustomTheme,
  validateCustomTheme,
  interpolateColor,
  getThemeTypeFromBackgroundColor,
  resolveColor,
} from './theme.js';
import type { SemanticColors } from './semantic-tokens.js';
import {
  DEFAULT_BACKGROUND_OPACITY,
  DEFAULT_INPUT_BACKGROUND_OPACITY,
  DEFAULT_SELECTION_OPACITY,
  DEFAULT_BORDER_OPACITY,
} from '../constants.js';
import { ANSI } from './builtin/dark/ansi-dark.js';
import { ANSILight } from './builtin/light/ansi-light.js';
import { NoColorTheme } from './builtin/no-color.js';
import process from 'node:process';
import { debugLogger, homedir } from '@google/gemini-cli-core';

export interface ThemeDisplay {
  name: string;
  type: ThemeType;
  isCustom?: boolean;
}

export const DEFAULT_THEME: Theme = DefaultDark;

class ThemeManager {
  private readonly availableThemes: Theme[];
  private activeTheme: Theme;
  private settingsThemes: Map<string, Theme> = new Map();
  private extensionThemes: Map<string, Theme> = new Map();
  private fileThemes: Map<string, Theme> = new Map();
  private terminalBackground: string | undefined;

  // Cache for dynamic colors
  private cachedColors: ColorsTheme | undefined;
  private cachedSemanticColors: SemanticColors | undefined;
  private lastCacheKey: string | undefined;

  private fs: typeof fs;
  private homedir: () => string;

  constructor(dependencies?: { fs?: typeof fs; homedir?: () => string }) {
    this.fs = dependencies?.fs ?? fs;
    this.homedir = dependencies?.homedir ?? homedir;

    this.availableThemes = [
      AyuDark,
      AyuLight,
      AtomOneDark,
      Dracula,
      DefaultLight,
      DefaultDark,
      GitHubDark,
      GitHubLight,
      GitHubDarkColorblind,
      GitHubLightColorblind,
      GoogleCode,
      Holiday,
      ShadesOfPurple,
      SolarizedDark,
      SolarizedLight,
      XCode,
      TokyoNight,
      ANSI,
      ANSILight,
    ];
    this.activeTheme = DEFAULT_THEME;
  }

  setTerminalBackground(color: string | undefined): void {
    if (this.terminalBackground !== color) {
      this.terminalBackground = color;
      this.clearCache();
    }
  }

  getTerminalBackground(): string | undefined {
    return this.terminalBackground;
  }

  private clearCache(): void {
    this.cachedColors = undefined;
    this.cachedSemanticColors = undefined;
    this.lastCacheKey = undefined;
  }

  isDefaultTheme(themeName: string | undefined): boolean {
    return (
      themeName === undefined ||
      themeName === DEFAULT_THEME.name ||
      themeName === DefaultLight.name
    );
  }

  /**
   * Loads custom themes from settings.
   * @param customThemesSettings Custom themes from settings.
   */
  loadCustomThemes(customThemesSettings?: Record<string, CustomTheme>): void {
    this.settingsThemes.clear();

    if (!customThemesSettings) {
      return;
    }

    for (const [name, customThemeConfig] of Object.entries(
      customThemesSettings,
    )) {
      const validation = validateCustomTheme(customThemeConfig);
      if (validation.isValid) {
        if (validation.warning) {
          debugLogger.warn(`Theme "${name}": ${validation.warning}`);
        }
        const themeWithDefaults: CustomTheme = {
          ...DEFAULT_THEME.colors,
          ...customThemeConfig,
          name: customThemeConfig.name || name,
          type: 'custom',
        };

        try {
          const theme = createCustomTheme(themeWithDefaults);
          this.settingsThemes.set(name, theme);
        } catch (error) {
          debugLogger.warn(`Failed to load custom theme "${name}":`, error);
        }
      } else {
        debugLogger.warn(`Invalid custom theme "${name}": ${validation.error}`);
      }
    }
    // If the current active theme is a settings theme, keep it if still valid
    if (
      this.activeTheme &&
      this.activeTheme.type === 'custom' &&
      this.settingsThemes.has(this.activeTheme.name)
    ) {
      this.activeTheme = this.settingsThemes.get(this.activeTheme.name)!;
    }
  }

  /**
   * Loads custom themes from extensions.
   * @param extensionName The name of the extension providing the themes.
   * @param customThemes Custom themes from extensions.
   */
  registerExtensionThemes(
    extensionName: string,
    customThemes?: CustomTheme[],
  ): void {
    if (!customThemes) {
      return;
    }

    for (const customThemeConfig of customThemes) {
      const namespacedName = `${customThemeConfig.name} (${extensionName})`;

      // Check for collisions with built-in themes (unlikely with prefix, but safe)
      if (this.availableThemes.some((t) => t.name === namespacedName)) {
        debugLogger.warn(
          `Theme name collision: "${namespacedName}" is a built-in theme. Skipping.`,
        );
        continue;
      }

      const validation = validateCustomTheme(customThemeConfig);
      if (validation.isValid) {
        if (validation.warning) {
          debugLogger.warn(`Theme "${namespacedName}": ${validation.warning}`);
        }
        const themeWithDefaults: CustomTheme = {
          ...DEFAULT_THEME.colors,
          ...customThemeConfig,
          name: namespacedName,
          type: 'custom',
        };

        try {
          const theme = createCustomTheme(themeWithDefaults);
          this.extensionThemes.set(namespacedName, theme);
        } catch (error) {
          debugLogger.warn(
            `Failed to load custom theme "${namespacedName}":`,
            error,
          );
        }
      } else {
        debugLogger.warn(
          `Invalid custom theme "${namespacedName}": ${validation.error}`,
        );
      }
    }
  }

  /**
   * Unregisters custom themes from extensions.
   * @param extensionName The name of the extension.
   * @param customThemes Custom themes to unregister.
   */
  unregisterExtensionThemes(
    extensionName: string,
    customThemes?: CustomTheme[],
  ): void {
    if (!customThemes) {
      return;
    }

    for (const theme of customThemes) {
      const namespacedName = `${theme.name} (${extensionName})`;
      this.extensionThemes.delete(namespacedName);
    }
  }

  /**
   * Checks if themes for a given extension are already registered.
   * @param extensionName The name of the extension.
   * @returns True if any themes from the extension are registered.
   */
  hasExtensionThemes(extensionName: string): boolean {
    return Array.from(this.extensionThemes.keys()).some((name) =>
      name.endsWith(`(${extensionName})`),
    );
  }

  /**
   * Clears all registered extension themes.
   * This is primarily for testing purposes to reset state between tests.
   */
  clearExtensionThemes(): void {
    this.extensionThemes.clear();
  }

  /**
   * Clears all themes loaded from files.
   * This is primarily for testing purposes to reset state between tests.
   */
  clearFileThemes(): void {
    this.fileThemes.clear();
  }

  /**
   * Re-initializes the ThemeManager with new dependencies.
   * This is primarily for testing to allow injecting mocks.
   */
  reinitialize(dependencies: { fs?: typeof fs; homedir?: () => string }): void {
    if (dependencies.fs) {
      this.fs = dependencies.fs;
    }
    if (dependencies.homedir) {
      this.homedir = dependencies.homedir;
    }
  }

  /**
   * Resets the ThemeManager state to defaults.
   * This is for testing purposes to ensure test isolation.
   */
  resetForTesting(dependencies?: {
    fs?: typeof fs;
    homedir?: () => string;
  }): void {
    if (dependencies) {
      this.reinitialize(dependencies);
    }
    this.settingsThemes.clear();
    this.extensionThemes.clear();
    this.fileThemes.clear();
    this.activeTheme = DEFAULT_THEME;
    this.terminalBackground = undefined;
    this.clearCache();
  }
  setActiveTheme(themeName: string | undefined): boolean {
    const theme = this.findThemeByName(themeName);
    if (!theme) {
      return false;
    }
    if (this.activeTheme !== theme) {
      this.activeTheme = theme;
      this.clearCache();
    }
    return true;
  }

  /**
   * Gets the currently active theme.
   * @returns The active theme.
   */
  getActiveTheme(): Theme {
    if (process.env['NO_COLOR']) {
      return NoColorTheme;
    }

    if (this.activeTheme) {
      const isBuiltIn = this.availableThemes.some(
        (t) => t.name === this.activeTheme.name,
      );
      const isCustom =
        [...this.settingsThemes.values()].includes(this.activeTheme) ||
        [...this.extensionThemes.values()].includes(this.activeTheme) ||
        [...this.fileThemes.values()].includes(this.activeTheme);

      if (isBuiltIn || isCustom) {
        return this.activeTheme;
      }

      // If the theme object is no longer valid, try to find it again by name.
      // This handles the case where extensions are reloaded and theme objects
      // are re-created.
      const reloadedTheme = this.findThemeByName(this.activeTheme.name);
      if (reloadedTheme) {
        this.activeTheme = reloadedTheme;
        return this.activeTheme;
      }
    }

    // Fallback to default if no active theme or if it's no longer valid.
    this.activeTheme = DEFAULT_THEME;
    return this.activeTheme;
  }

  /**
   * Gets the colors for the active theme, respecting the terminal background.
   * @returns The theme colors.
   */
  getColors(): ColorsTheme {
    const activeTheme = this.getActiveTheme();
    const cacheKey = `${activeTheme.name}:${this.terminalBackground}`;
    if (this.cachedColors && this.lastCacheKey === cacheKey) {
      return this.cachedColors;
    }

    const colors = activeTheme.colors;
    if (
      this.terminalBackground &&
      this.isThemeCompatible(activeTheme, this.terminalBackground)
    ) {
      this.cachedColors = {
        ...colors,
        Background: this.terminalBackground,
        DarkGray: interpolateColor(
          this.terminalBackground,
          colors.Gray,
          DEFAULT_BORDER_OPACITY,
        ),
        InputBackground: interpolateColor(
          this.terminalBackground,
          colors.Gray,
          DEFAULT_INPUT_BACKGROUND_OPACITY,
        ),
        MessageBackground: interpolateColor(
          this.terminalBackground,
          colors.Gray,
          DEFAULT_BACKGROUND_OPACITY,
        ),
        FocusBackground: interpolateColor(
          this.terminalBackground,
          activeTheme.colors.FocusColor ?? activeTheme.colors.AccentGreen,
          DEFAULT_SELECTION_OPACITY,
        ),
      };
    } else {
      this.cachedColors = colors;
    }

    this.lastCacheKey = cacheKey;
    return this.cachedColors;
  }

  /**
   * Gets the semantic colors for the active theme.
   * @returns The semantic colors.
   */
  getSemanticColors(): SemanticColors {
    const activeTheme = this.getActiveTheme();
    const cacheKey = `${activeTheme.name}:${this.terminalBackground}`;
    if (this.cachedSemanticColors && this.lastCacheKey === cacheKey) {
      return this.cachedSemanticColors;
    }

    const semanticColors = activeTheme.semanticColors;
    if (
      this.terminalBackground &&
      this.isThemeCompatible(activeTheme, this.terminalBackground)
    ) {
      const colors = this.getColors();
      this.cachedSemanticColors = {
        ...semanticColors,
        background: {
          ...semanticColors.background,
          primary: this.terminalBackground,
          message: colors.MessageBackground!,
          input: colors.InputBackground!,
          focus: colors.FocusBackground!,
        },
        border: {
          ...semanticColors.border,
          default: colors.DarkGray,
        },
        ui: {
          ...semanticColors.ui,
          dark: colors.DarkGray,
          focus: colors.FocusColor ?? colors.AccentGreen,
        },
      };
    } else {
      this.cachedSemanticColors = semanticColors;
    }

    this.lastCacheKey = cacheKey;
    return this.cachedSemanticColors;
  }

  isThemeCompatible(
    activeTheme: Theme,
    terminalBackground: string | undefined,
  ): boolean {
    if (activeTheme.type === 'ansi') {
      return true;
    }

    const backgroundType = getThemeTypeFromBackgroundColor(terminalBackground);
    if (!backgroundType) {
      return true;
    }

    const themeType =
      activeTheme.type === 'custom'
        ? getThemeTypeFromBackgroundColor(
            resolveColor(activeTheme.colors.Background) ||
              activeTheme.colors.Background,
          )
        : activeTheme.type;

    return themeType === backgroundType;
  }

  private _getAllCustomThemes(): Theme[] {
    return [
      ...Array.from(this.settingsThemes.values()),
      ...Array.from(this.extensionThemes.values()),
      ...Array.from(this.fileThemes.values()),
    ];
  }

  /**
   * Gets a list of custom theme names.
   * @returns Array of custom theme names.
   */
  getCustomThemeNames(): string[] {
    return this._getAllCustomThemes().map((theme) => theme.name);
  }

  /**
   * Checks if a theme name is a custom theme.
   * @param themeName The theme name to check.
   * @returns True if the theme is custom.
   */
  isCustomTheme(themeName: string): boolean {
    return (
      this.settingsThemes.has(themeName) ||
      this.extensionThemes.has(themeName) ||
      this.fileThemes.has(themeName)
    );
  }

  /**
   * Returns a list of available theme names.
   */
  getAvailableThemes(): ThemeDisplay[] {
    const builtInThemes = this.availableThemes.map((theme) => ({
      name: theme.name,
      type: theme.type,
      isCustom: false,
    }));

    const customThemes = this._getAllCustomThemes().map((theme) => ({
      name: theme.name,
      type: theme.type,
      isCustom: true,
    }));

    const allThemes = [...builtInThemes, ...customThemes];

    const sortedThemes = allThemes.sort((a, b) => {
      const typeOrder = (type: ThemeType): number => {
        switch (type) {
          case 'dark':
            return 1;
          case 'light':
            return 2;
          case 'ansi':
            return 3;
          case 'custom':
            return 4; // Custom themes at the end
          default:
            return 5;
        }
      };

      const typeComparison = typeOrder(a.type) - typeOrder(b.type);
      if (typeComparison !== 0) {
        return typeComparison;
      }
      return a.name.localeCompare(b.name);
    });

    return sortedThemes;
  }

  /**
   * Gets a theme by name.
   * @param themeName The name of the theme to get.
   * @returns The theme if found, undefined otherwise.
   */
  getTheme(themeName: string): Theme | undefined {
    return this.findThemeByName(themeName);
  }

  /**
   * Gets all available themes.
   * @returns A list of all available themes.
   */
  getAllThemes(): Theme[] {
    return [...this.availableThemes, ...this._getAllCustomThemes()];
  }

  private isPath(themeName: string): boolean {
    return (
      themeName.endsWith('.json') ||
      themeName.startsWith('.') ||
      path.isAbsolute(themeName)
    );
  }

  private loadThemeFromFile(themePath: string): Theme | undefined {
    try {
      // realpathSync resolves the path and throws if it doesn't exist.
      const canonicalPath = this.fs.realpathSync(path.resolve(themePath));

      // 1. Check cache using the canonical path.
      if (this.fileThemes.has(canonicalPath)) {
        return this.fileThemes.get(canonicalPath);
      }

      // 2. Perform security check.
      const homeDir = path.resolve(this.homedir());
      if (!canonicalPath.startsWith(homeDir)) {
        debugLogger.warn(
          `Theme file at "${themePath}" is outside your home directory. ` +
            `Only load themes from trusted sources.`,
        );
        return undefined;
      }

      // 3. Read, parse, and validate the theme file.
      const themeContent = this.fs.readFileSync(canonicalPath, 'utf-8');
      // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
      const customThemeConfig = JSON.parse(themeContent) as CustomTheme;

      const validation = validateCustomTheme(customThemeConfig);
      if (!validation.isValid) {
        debugLogger.warn(
          `Invalid custom theme from file "${themePath}": ${validation.error}`,
        );
        return undefined;
      }

      if (validation.warning) {
        debugLogger.warn(`Theme from "${themePath}": ${validation.warning}`);
      }

      // 4. Create and cache the theme.
      const themeWithDefaults: CustomTheme = {
        ...DEFAULT_THEME.colors,
        ...customThemeConfig,
        name: customThemeConfig.name || canonicalPath,
        type: 'custom',
      };

      const theme = createCustomTheme(themeWithDefaults);
      this.fileThemes.set(canonicalPath, theme); // Cache by canonical path
      return theme;
    } catch (error) {
      // Any error in the process (file not found, bad JSON, etc.) is caught here.
      // We can return undefined silently for file-not-found, and warn for others.
      if (
        !(error instanceof Error && 'code' in error && error.code === 'ENOENT')
      ) {
        debugLogger.warn(
          `Could not load theme from file "${themePath}":`,
          error,
        );
      }
      return undefined;
    }
  }

  findThemeByName(themeName: string | undefined): Theme | undefined {
    if (!themeName) {
      return DEFAULT_THEME;
    }

    // First check built-in themes
    const builtInTheme = this.availableThemes.find(
      (theme) => theme.name === themeName,
    );
    if (builtInTheme) {
      return builtInTheme;
    }

    // Then check custom themes that have been loaded from settings, extensions, or file paths
    if (this.isPath(themeName)) {
      return this.loadThemeFromFile(themeName);
    }

    if (this.settingsThemes.has(themeName)) {
      return this.settingsThemes.get(themeName);
    }

    if (this.extensionThemes.has(themeName)) {
      return this.extensionThemes.get(themeName);
    }

    if (this.fileThemes.has(themeName)) {
      return this.fileThemes.get(themeName);
    }

    // If it's not a built-in, not in cache, and not a valid file path,
    // it's not a valid theme.
    return undefined;
  }
}

// Export an instance of the ThemeManager
export const themeManager = new ThemeManager();
