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

import type { CSSProperties } from 'react';

import type { SemanticColors } from './semantic-tokens.js';

import type { CustomTheme } from '@google/gemini-cli-core';
import {
  DEFAULT_INPUT_BACKGROUND_OPACITY,
  DEFAULT_SELECTION_OPACITY,
  DEFAULT_BORDER_OPACITY,
} from '../constants.js';
import tinygradient from 'tinygradient';
import tinycolor from 'tinycolor2';

// Define the set of Ink's named colors for quick lookup
export const INK_SUPPORTED_NAMES = new Set([
  'black',
  'red',
  'green',
  'yellow',
  'blue',
  'cyan',
  'magenta',
  'white',
  'gray',
  'grey',
  'blackbright',
  'redbright',
  'greenbright',
  'yellowbright',
  'bluebright',
  'cyanbright',
  'magentabright',
  'whitebright',
]);

// Use tinycolor's built-in names map for CSS colors, excluding ones Ink supports
export const CSS_NAME_TO_HEX_MAP = Object.fromEntries(
  Object.entries(tinycolor.names)
    .filter(([name]) => !INK_SUPPORTED_NAMES.has(name))
    .map(([name, hex]) => [name, `#${hex}`]),
);

// Mapping for ANSI bright colors that are not in tinycolor's standard CSS names
export const INK_NAME_TO_HEX_MAP: Readonly<Record<string, string>> = {
  blackbright: '#555555',
  redbright: '#ff5555',
  greenbright: '#55ff55',
  yellowbright: '#ffff55',
  bluebright: '#5555ff',
  magentabright: '#ff55ff',
  cyanbright: '#55ffff',
  whitebright: '#ffffff',
};

/**
 * Calculates the relative luminance of a color.
 * See https://www.w3.org/TR/WCAG20/#relativeluminancedef
 *
 * @param color Color string (hex or Ink-supported name)
 * @returns Luminance value (0-255)
 */
export function getLuminance(color: string): number {
  const resolved = color.toLowerCase();
  const hex = INK_NAME_TO_HEX_MAP[resolved] || resolved;

  const colorObj = tinycolor(hex);
  if (!colorObj.isValid()) {
    return 0;
  }

  // tinycolor returns 0-1, we need 0-255
  return colorObj.getLuminance() * 255;
}

/**
 * Resolves a CSS color value (name or hex) into an Ink-compatible color string.
 * @param colorValue The raw color string (e.g., 'blue', '#ff0000', 'darkkhaki').
 * @returns An Ink-compatible color string (hex or name), or undefined if not resolvable.
 */
export function resolveColor(colorValue: string): string | undefined {
  const lowerColor = colorValue.toLowerCase();

  // 1. Check if it's already a hex code and valid
  if (lowerColor.startsWith('#')) {
    if (/^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(colorValue)) {
      return lowerColor;
    } else {
      return undefined;
    }
  }

  // Handle hex codes without #
  if (/^[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(colorValue)) {
    return `#${lowerColor}`;
  }

  // 2. Check if it's an Ink supported name (lowercase)
  if (INK_SUPPORTED_NAMES.has(lowerColor)) {
    return lowerColor; // Use Ink name directly
  }

  // 3. Check if it's a known CSS name we can map to hex
  // We can't import CSS_NAME_TO_HEX_MAP here due to circular deps,
  // but we can use tinycolor directly for named colors.
  const colorObj = tinycolor(lowerColor);
  if (colorObj.isValid()) {
    return colorObj.toHexString();
  }

  // 4. Could not resolve
  return undefined;
}

export function interpolateColor(
  color1: string,
  color2: string,
  factor: number,
) {
  if (factor <= 0 && color1) {
    return color1;
  }
  if (factor >= 1 && color2) {
    return color2;
  }
  if (!color1 || !color2) {
    return '';
  }
  try {
    const gradient = tinygradient(color1, color2);
    const color = gradient.rgbAt(factor);
    return color.toHexString();
  } catch {
    return color1;
  }
}

export function getThemeTypeFromBackgroundColor(
  backgroundColor: string | undefined,
): 'light' | 'dark' | undefined {
  if (!backgroundColor) {
    return undefined;
  }

  const resolvedColor = resolveColor(backgroundColor);
  if (!resolvedColor) {
    return undefined;
  }

  const luminance = getLuminance(resolvedColor);
  return luminance > 128 ? 'light' : 'dark';
}

export type { CustomTheme };

export type ThemeType = 'light' | 'dark' | 'ansi' | 'custom';

export interface ColorsTheme {
  type: ThemeType;
  Background: string;
  Foreground: string;
  LightBlue: string;
  AccentBlue: string;
  AccentPurple: string;
  AccentCyan: string;
  AccentGreen: string;
  AccentYellow: string;
  AccentRed: string;
  DiffAdded: string;
  DiffRemoved: string;
  Comment: string;
  Gray: string;
  DarkGray: string;
  InputBackground?: string;
  MessageBackground?: string;
  FocusBackground?: string;
  FocusColor?: string;
  GradientColors?: string[];
}

export const lightTheme: ColorsTheme = {
  type: 'light',
  Background: '#FFFFFF',
  Foreground: '#000000',
  LightBlue: '#005FAF',
  AccentBlue: '#005FAF',
  AccentPurple: '#5F00FF',
  AccentCyan: '#005F87',
  AccentGreen: '#005F00',
  AccentYellow: '#875F00',
  AccentRed: '#AF0000',
  DiffAdded: '#D7FFD7',
  DiffRemoved: '#FFD7D7',
  Comment: '#008700',
  Gray: '#5F5F5F',
  DarkGray: '#5F5F5F',
  InputBackground: '#E4E4E4',
  MessageBackground: '#FAFAFA',
  FocusBackground: '#D7FFD7',
  GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
};

export const darkTheme: ColorsTheme = {
  type: 'dark',
  Background: '#000000',
  Foreground: '#FFFFFF',
  LightBlue: '#AFD7D7',
  AccentBlue: '#87AFFF',
  AccentPurple: '#D7AFFF',
  AccentCyan: '#87D7D7',
  AccentGreen: '#D7FFD7',
  AccentYellow: '#FFFFAF',
  AccentRed: '#FF87AF',
  DiffAdded: '#005F00',
  DiffRemoved: '#5F0000',
  Comment: '#AFAFAF',
  Gray: '#AFAFAF',
  DarkGray: '#878787',
  InputBackground: '#5F5F5F',
  MessageBackground: '#5F5F5F',
  FocusBackground: '#005F00',
  GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
};

export const ansiTheme: ColorsTheme = {
  type: 'ansi',
  Background: 'black',
  Foreground: '',
  LightBlue: 'blue',
  AccentBlue: 'blue',
  AccentPurple: 'magenta',
  AccentCyan: 'cyan',
  AccentGreen: 'green',
  AccentYellow: 'yellow',
  AccentRed: 'red',
  DiffAdded: 'green',
  DiffRemoved: 'red',
  Comment: 'gray',
  Gray: 'gray',
  DarkGray: 'gray',
  InputBackground: 'black',
  MessageBackground: 'black',
  FocusBackground: 'black',
};

export class Theme {
  /**
   * The default foreground color for text when no specific highlight rule applies.
   * This is an Ink-compatible color string (hex or name).
   */
  readonly defaultColor: string;
  /**
   * Stores the mapping from highlight.js class names (e.g., 'hljs-keyword')
   * to Ink-compatible color strings (hex or name).
   */
  protected readonly _colorMap: Readonly<Record<string, string>>;
  readonly semanticColors: SemanticColors;

  /**
   * Creates a new Theme instance.
   * @param name The name of the theme.
   * @param rawMappings The raw CSSProperties mappings from a react-syntax-highlighter theme object.
   */
  constructor(
    readonly name: string,
    readonly type: ThemeType,
    rawMappings: Record<string, CSSProperties>,
    readonly colors: ColorsTheme,
    semanticColors?: SemanticColors,
  ) {
    this.semanticColors = semanticColors ?? {
      text: {
        primary: this.colors.Foreground,
        secondary: this.colors.Gray,
        link: this.colors.AccentBlue,
        accent: this.colors.AccentPurple,
        response: this.colors.Foreground,
      },
      background: {
        primary: this.colors.Background,
        message:
          this.colors.MessageBackground ??
          interpolateColor(
            this.colors.Background,
            this.colors.Gray,
            DEFAULT_INPUT_BACKGROUND_OPACITY,
          ),
        input:
          this.colors.InputBackground ??
          interpolateColor(
            this.colors.Background,
            this.colors.Gray,
            DEFAULT_INPUT_BACKGROUND_OPACITY,
          ),
        focus:
          this.colors.FocusBackground ??
          interpolateColor(
            this.colors.Background,
            this.colors.FocusColor ?? this.colors.AccentGreen,
            DEFAULT_SELECTION_OPACITY,
          ),
        diff: {
          added: this.colors.DiffAdded,
          removed: this.colors.DiffRemoved,
        },
      },
      border: {
        default: this.colors.DarkGray,
      },
      ui: {
        comment: this.colors.Gray,
        symbol: this.colors.AccentCyan,
        active: this.colors.AccentBlue,
        dark: this.colors.DarkGray,
        focus: this.colors.FocusColor ?? this.colors.AccentGreen,
        gradient: this.colors.GradientColors,
      },
      status: {
        error: this.colors.AccentRed,
        success: this.colors.AccentGreen,
        warning: this.colors.AccentYellow,
      },
    };
    this._colorMap = Object.freeze(this._buildColorMap(rawMappings)); // Build and freeze the map

    // Determine the default foreground color
    const rawDefaultColor = rawMappings['hljs']?.color;
    this.defaultColor =
      (rawDefaultColor ? Theme._resolveColor(rawDefaultColor) : undefined) ??
      ''; // Default to empty string if not found or resolvable
  }

  /**
   * Gets the Ink-compatible color string for a given highlight.js class name.
   * @param hljsClass The highlight.js class name (e.g., 'hljs-keyword', 'hljs-string').
   * @returns The corresponding Ink color string (hex or name) if it exists.
   */
  getInkColor(hljsClass: string): string | undefined {
    return this._colorMap[hljsClass];
  }

  /**
   * Resolves a CSS color value (name or hex) into an Ink-compatible color string.
   * @param colorValue The raw color string (e.g., 'blue', '#ff0000', 'darkkhaki').
   * @returns An Ink-compatible color string (hex or name), or undefined if not resolvable.
   */
  private static _resolveColor(colorValue: string): string | undefined {
    return resolveColor(colorValue);
  }

  /**
   * Builds the internal map from highlight.js class names to Ink-compatible color strings.
   * This method is protected and primarily intended for use by the constructor.
   * @param hljsTheme The raw CSSProperties mappings from a react-syntax-highlighter theme object.
   * @returns An Ink-compatible theme map (Record<string, string>).
   */
  protected _buildColorMap(
    hljsTheme: Record<string, CSSProperties>,
  ): Record<string, string> {
    const inkTheme: Record<string, string> = {};
    for (const key in hljsTheme) {
      // Ensure the key starts with 'hljs-' or is 'hljs' for the base style
      if (!key.startsWith('hljs-') && key !== 'hljs') {
        continue; // Skip keys not related to highlighting classes
      }

      const style = hljsTheme[key];
      if (style?.color) {
        const resolvedColor = Theme._resolveColor(style.color);
        if (resolvedColor !== undefined) {
          // Use the original key from the hljsTheme (e.g., 'hljs-keyword')
          inkTheme[key] = resolvedColor;
        }
        // If color is not resolvable, it's omitted from the map,
        // this enables falling back to the default foreground color.
      }
      // We currently only care about the 'color' property for Ink rendering.
      // Other properties like background, fontStyle, etc., are ignored.
    }
    return inkTheme;
  }
}

/**
 * Creates a Theme instance from a custom theme configuration.
 * @param customTheme The custom theme configuration.
 * @returns A new Theme instance.
 */
export function createCustomTheme(customTheme: CustomTheme): Theme {
  const colors: ColorsTheme = {
    type: 'custom',
    Background: customTheme.background?.primary ?? customTheme.Background ?? '',
    Foreground: customTheme.text?.primary ?? customTheme.Foreground ?? '',
    LightBlue: customTheme.text?.link ?? customTheme.LightBlue ?? '',
    AccentBlue: customTheme.text?.link ?? customTheme.AccentBlue ?? '',
    AccentPurple: customTheme.text?.accent ?? customTheme.AccentPurple ?? '',
    AccentCyan: customTheme.text?.link ?? customTheme.AccentCyan ?? '',
    AccentGreen: customTheme.status?.success ?? customTheme.AccentGreen ?? '',
    AccentYellow: customTheme.status?.warning ?? customTheme.AccentYellow ?? '',
    AccentRed: customTheme.status?.error ?? customTheme.AccentRed ?? '',
    DiffAdded:
      customTheme.background?.diff?.added ?? customTheme.DiffAdded ?? '',
    DiffRemoved:
      customTheme.background?.diff?.removed ?? customTheme.DiffRemoved ?? '',
    Comment: customTheme.ui?.comment ?? customTheme.Comment ?? '',
    Gray: customTheme.text?.secondary ?? customTheme.Gray ?? '',
    DarkGray:
      customTheme.DarkGray ??
      interpolateColor(
        customTheme.background?.primary ?? customTheme.Background ?? '',
        customTheme.text?.secondary ?? customTheme.Gray ?? '',
        DEFAULT_BORDER_OPACITY,
      ),
    InputBackground: interpolateColor(
      customTheme.background?.primary ?? customTheme.Background ?? '',
      customTheme.text?.secondary ?? customTheme.Gray ?? '',
      DEFAULT_INPUT_BACKGROUND_OPACITY,
    ),
    MessageBackground: interpolateColor(
      customTheme.background?.primary ?? customTheme.Background ?? '',
      customTheme.text?.secondary ?? customTheme.Gray ?? '',
      DEFAULT_INPUT_BACKGROUND_OPACITY,
    ),
    FocusBackground: interpolateColor(
      customTheme.background?.primary ?? customTheme.Background ?? '',
      customTheme.status?.success ?? customTheme.AccentGreen ?? '#3CA84B', // Fallback to a default green if not found
      DEFAULT_SELECTION_OPACITY,
    ),
    FocusColor: customTheme.ui?.focus ?? customTheme.AccentGreen,
    GradientColors: customTheme.ui?.gradient ?? customTheme.GradientColors,
  };

  // Generate CSS properties mappings based on the custom theme colors
  const rawMappings: Record<string, CSSProperties> = {
    hljs: {
      display: 'block',
      overflowX: 'auto',
      padding: '0.5em',
      background: colors.Background,
      color: colors.Foreground,
    },
    'hljs-keyword': {
      color: colors.AccentBlue,
    },
    'hljs-literal': {
      color: colors.AccentBlue,
    },
    'hljs-symbol': {
      color: colors.AccentBlue,
    },
    'hljs-name': {
      color: colors.AccentBlue,
    },
    'hljs-link': {
      color: colors.AccentBlue,
      textDecoration: 'underline',
    },
    'hljs-built_in': {
      color: colors.AccentCyan,
    },
    'hljs-type': {
      color: colors.AccentCyan,
    },
    'hljs-number': {
      color: colors.AccentGreen,
    },
    'hljs-class': {
      color: colors.AccentGreen,
    },
    'hljs-string': {
      color: colors.AccentYellow,
    },
    'hljs-meta-string': {
      color: colors.AccentYellow,
    },
    'hljs-regexp': {
      color: colors.AccentRed,
    },
    'hljs-template-tag': {
      color: colors.AccentRed,
    },
    'hljs-subst': {
      color: colors.Foreground,
    },
    'hljs-function': {
      color: colors.Foreground,
    },
    'hljs-title': {
      color: colors.Foreground,
    },
    'hljs-params': {
      color: colors.Foreground,
    },
    'hljs-formula': {
      color: colors.Foreground,
    },
    'hljs-comment': {
      color: colors.Comment,
      fontStyle: 'italic',
    },
    'hljs-quote': {
      color: colors.Comment,
      fontStyle: 'italic',
    },
    'hljs-doctag': {
      color: colors.Comment,
    },
    'hljs-meta': {
      color: colors.Gray,
    },
    'hljs-meta-keyword': {
      color: colors.Gray,
    },
    'hljs-tag': {
      color: colors.Gray,
    },
    'hljs-variable': {
      color: colors.AccentPurple,
    },
    'hljs-template-variable': {
      color: colors.AccentPurple,
    },
    'hljs-attr': {
      color: colors.LightBlue,
    },
    'hljs-attribute': {
      color: colors.LightBlue,
    },
    'hljs-builtin-name': {
      color: colors.LightBlue,
    },
    'hljs-section': {
      color: colors.AccentYellow,
    },
    'hljs-emphasis': {
      fontStyle: 'italic',
    },
    'hljs-strong': {
      fontWeight: 'bold',
    },
    'hljs-bullet': {
      color: colors.AccentYellow,
    },
    'hljs-selector-tag': {
      color: colors.AccentYellow,
    },
    'hljs-selector-id': {
      color: colors.AccentYellow,
    },
    'hljs-selector-class': {
      color: colors.AccentYellow,
    },
    'hljs-selector-attr': {
      color: colors.AccentYellow,
    },
    'hljs-selector-pseudo': {
      color: colors.AccentYellow,
    },
    'hljs-addition': {
      backgroundColor: colors.AccentGreen,
      display: 'inline-block',
      width: '100%',
    },
    'hljs-deletion': {
      backgroundColor: colors.AccentRed,
      display: 'inline-block',
      width: '100%',
    },
  };

  const semanticColors: SemanticColors = {
    text: {
      primary: customTheme.text?.primary ?? colors.Foreground,
      secondary: customTheme.text?.secondary ?? colors.Gray,
      link: customTheme.text?.link ?? colors.AccentBlue,
      accent: customTheme.text?.accent ?? colors.AccentPurple,
      response:
        customTheme.text?.response ??
        customTheme.text?.primary ??
        colors.Foreground,
    },
    background: {
      primary: customTheme.background?.primary ?? colors.Background,
      message: colors.MessageBackground!,
      input: colors.InputBackground!,
      focus: colors.FocusBackground!,
      diff: {
        added: customTheme.background?.diff?.added ?? colors.DiffAdded,
        removed: customTheme.background?.diff?.removed ?? colors.DiffRemoved,
      },
    },
    border: {
      default: colors.DarkGray,
    },
    ui: {
      comment: customTheme.ui?.comment ?? colors.Comment,
      symbol: customTheme.ui?.symbol ?? colors.Gray,
      active: customTheme.ui?.active ?? colors.AccentBlue,
      dark: colors.DarkGray,
      focus: colors.FocusColor ?? colors.AccentGreen,
      gradient: customTheme.ui?.gradient ?? colors.GradientColors,
    },
    status: {
      error: customTheme.status?.error ?? colors.AccentRed,
      success: customTheme.status?.success ?? colors.AccentGreen,
      warning: customTheme.status?.warning ?? colors.AccentYellow,
    },
  };

  return new Theme(
    customTheme.name,
    'custom',
    rawMappings,
    colors,
    semanticColors,
  );
}

/**
 * Validates a custom theme configuration.
 * @param customTheme The custom theme to validate.
 * @returns An object with isValid boolean and error message if invalid.
 */
export function validateCustomTheme(customTheme: Partial<CustomTheme>): {
  isValid: boolean;
  error?: string;
  warning?: string;
} {
  // Since all fields are optional, we only need to validate the name.
  if (customTheme.name && !isValidThemeName(customTheme.name)) {
    return {
      isValid: false,
      error: `Invalid theme name: ${customTheme.name}`,
    };
  }

  return {
    isValid: true,
  };
}

/**
 * Checks if a theme name is valid.
 * @param name The theme name to validate.
 * @returns True if the theme name is valid.
 */
function isValidThemeName(name: string): boolean {
  // Theme name should be non-empty and not contain invalid characters
  return name.trim().length > 0 && name.trim().length <= 50;
}

/**
 * Picks a default theme name based on terminal background color.
 * It first tries to find a theme with an exact background color match.
 * If no match is found, it falls back to a light or dark theme based on the
 * luminance of the background color.
 * @param terminalBackground The hex color string of the terminal background.
 * @param availableThemes A list of available themes to search through.
 * @param defaultDarkThemeName The name of the fallback dark theme.
 * @param defaultLightThemeName The name of the fallback light theme.
 * @returns The name of the chosen theme.
 */
export function pickDefaultThemeName(
  terminalBackground: string | undefined,
  availableThemes: readonly Theme[],
  defaultDarkThemeName: string,
  defaultLightThemeName: string,
): string {
  if (terminalBackground) {
    const lowerTerminalBackground = terminalBackground.toLowerCase();
    for (const theme of availableThemes) {
      if (!theme.colors.Background) continue;
      // resolveColor can return undefined
      const themeBg = resolveColor(theme.colors.Background)?.toLowerCase();
      if (themeBg === lowerTerminalBackground) {
        return theme.name;
      }
    }
  }

  const themeType = getThemeTypeFromBackgroundColor(terminalBackground);
  if (themeType === 'light') {
    return defaultLightThemeName;
  }

  return defaultDarkThemeName;
}
