import colors from "tailwindcss/colors"

import type {
  GalleryColorFamily,
  GalleryColorStep,
  GalleryColorTokenName,
  GalleryColorTokenValue,
  GalleryColorTokenValues,
  GalleryThemeCssVariables,
  GalleryThemePresetId,
} from "@/app/gallery/editor-panels"
import {
  galleryColorFamilies,
  galleryColorSteps,
  galleryColorTokenDefaults,
  galleryThemePresets,
} from "@/app/gallery/editor-panels"

type TailwindColorScale = Record<GalleryColorStep, string>

export type GalleryTokenThemeDraft = {
  colorTokenValues: GalleryColorTokenValues
  kind: "tokens"
}

export type GalleryPresetThemeDraft = {
  darkCssVariables: GalleryThemeCssVariables
  id: GalleryThemePresetId
  kind: "preset"
  lightCssVariables: GalleryThemeCssVariables
}

export type GalleryThemeDraft =
  | GalleryPresetThemeDraft
  | GalleryTokenThemeDraft

const appliedGalleryThemeStorageKey = "gallery:applied-theme"
const legacyAppliedGalleryThemeStorageKey = "gallery:applied-color-token-values"
const galleryAppliedThemeStyleId = "gallery-applied-theme-vars"
const galleryColorFamilySet = new Set<string>(galleryColorFamilies)
const galleryColorStepSet = new Set<string>(galleryColorSteps)
const managedRootVariableNames = [
  "--background",
  "--foreground",
  "--card",
  "--card-foreground",
  "--popover",
  "--popover-foreground",
  "--primary",
  "--primary-foreground",
  "--secondary",
  "--secondary-foreground",
  "--muted",
  "--muted-foreground",
  "--accent",
  "--accent-foreground",
  "--destructive",
  "--destructive-foreground",
  "--border",
  "--input",
  "--ring",
  "--chart-1",
  "--chart-2",
  "--chart-3",
  "--chart-4",
  "--chart-5",
  "--sidebar",
  "--sidebar-foreground",
  "--sidebar-primary",
  "--sidebar-primary-foreground",
  "--sidebar-accent",
  "--sidebar-accent-foreground",
  "--sidebar-border",
  "--sidebar-ring",
  "--font-sans",
  "--font-serif",
  "--font-mono",
  "--radius",
  "--shadow-x",
  "--shadow-y",
  "--shadow-blur",
  "--shadow-spread",
  "--shadow-opacity",
  "--shadow-color",
  "--shadow-2xs",
  "--shadow-xs",
  "--shadow-sm",
  "--shadow",
  "--shadow-md",
  "--shadow-lg",
  "--shadow-xl",
  "--shadow-2xl",
  "--tracking-normal",
  "--spacing",
] as const

const tailwindColorFamilies = Object.fromEntries(
  (Object.keys(colors) as GalleryColorFamily[]).map((family) => [
    family,
    colors[family] as TailwindColorScale,
  ])
) as Record<GalleryColorFamily, TailwindColorScale>

type RgbColor = {
  blue: number
  green: number
  red: number
}

function parseHexColor(value: string | undefined): RgbColor | null {
  if (!value) {
    return null
  }

  const hexMatch = value.trim().match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/)
  if (!hexMatch) {
    return null
  }

  const hexValue = hexMatch[1]
  const normalizedHex =
    hexValue.length === 3
      ? hexValue
          .split("")
          .map((character) => `${character}${character}`)
          .join("")
      : hexValue

  return {
    blue: Number.parseInt(normalizedHex.slice(4, 6), 16),
    green: Number.parseInt(normalizedHex.slice(2, 4), 16),
    red: Number.parseInt(normalizedHex.slice(0, 2), 16),
  }
}

function clampColorChannel(value: number) {
  return Math.min(255, Math.max(0, Math.round(value * 255)))
}

function linearSrgbToSrgb(value: number) {
  return value >= 0.0031308
    ? 1.055 * value ** (1 / 2.4) - 0.055
    : 12.92 * value
}

function parseOklchColor(value: string | undefined): RgbColor | null {
  if (!value) {
    return null
  }

  const match = value
    .trim()
    .match(
      /^oklch\(\s*([0-9.]+)%?\s+([0-9.]+)\s+([0-9.]+|none)(?:deg)?(?:\s*\/\s*[0-9.]+%?)?\s*\)$/i
    )
  if (!match) {
    return null
  }

  const lightnessValue = Number.parseFloat(match[1])
  const lightness = match[1].includes("%")
    ? lightnessValue / 100
    : lightnessValue > 1
      ? lightnessValue / 100
      : lightnessValue
  const chroma = Number.parseFloat(match[2])
  const hue = match[3].toLowerCase() === "none" ? 0 : Number.parseFloat(match[3])
  const hueRadians = (hue * Math.PI) / 180

  const a = chroma * Math.cos(hueRadians)
  const b = chroma * Math.sin(hueRadians)
  const lPrime = lightness + 0.3963377774 * a + 0.2158037573 * b
  const mPrime = lightness - 0.1055613458 * a - 0.0638541728 * b
  const sPrime = lightness - 0.0894841775 * a - 1.291485548 * b
  const l = lPrime ** 3
  const m = mPrime ** 3
  const s = sPrime ** 3

  return {
    blue: clampColorChannel(
      linearSrgbToSrgb(
        0.055710120445510616 * l -
          0.2040210505984867 * m +
          1.0572251689370704 * s
      )
    ),
    green: clampColorChannel(
      linearSrgbToSrgb(
        -1.2684380040921763 * l +
          2.6097574006633715 * m -
          0.3413193963102197 * s
      )
    ),
    red: clampColorChannel(
      linearSrgbToSrgb(
        4.076741661347994 * l -
          3.307711590408193 * m +
          0.230969928729428 * s
      )
    ),
  }
}

function parseColor(value: string | undefined): RgbColor | null {
  return parseHexColor(value) ?? parseOklchColor(value)
}

const tailwindColorEntries = galleryColorFamilies.flatMap((family) =>
  galleryColorSteps.flatMap((step) => {
    const color = parseColor(tailwindColorFamilies[family]?.[step])

    return color
      ? [
          {
            color,
            value: { family, step },
          },
        ]
      : []
  })
)

function getColorDistance(left: RgbColor, right: RgbColor) {
  return (
    (left.red - right.red) ** 2 +
    (left.green - right.green) ** 2 +
    (left.blue - right.blue) ** 2
  )
}

function findNearestTailwindColor(value: string): GalleryColorTokenValue | null {
  const color = parseColor(value)
  if (!color) {
    return null
  }

  let nearestValue: GalleryColorTokenValue | null = null
  let nearestDistance = Number.POSITIVE_INFINITY

  for (const entry of tailwindColorEntries) {
    const distance = getColorDistance(color, entry.color)
    if (distance < nearestDistance) {
      nearestDistance = distance
      nearestValue = entry.value
    }
  }

  return nearestValue
}

function resolveTokenColor(
  values: GalleryColorTokenValues,
  tokenName: keyof GalleryColorTokenValues
) {
  const token = values[tokenName]
  return (
    tailwindColorFamilies[token.family]?.[token.step] ??
    tailwindColorFamilies.zinc[500]
  )
}

function resolveTokenValueColor(value: GalleryColorTokenValue) {
  return (
    tailwindColorFamilies[value.family]?.[value.step] ??
    tailwindColorFamilies.zinc[500]
  )
}

function isCssVariables(value: unknown): value is GalleryThemeCssVariables {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return false
  }

  return Object.entries(value).every(([name, cssValue]) => {
    return (
      name.startsWith("--") &&
      typeof cssValue === "string" &&
      cssValue.trim().length > 0
    )
  })
}

function getManagedThemeStyleElement() {
  let styleElement = document.getElementById(galleryAppliedThemeStyleId)

  if (!styleElement) {
    styleElement = document.createElement("style")
    styleElement.id = galleryAppliedThemeStyleId
    document.head.appendChild(styleElement)
  }

  return styleElement
}

function formatCssVariables(variables: GalleryThemeCssVariables) {
  return Object.entries(variables)
    .map(([name, value]) => `  ${name}: ${value};`)
    .join("\n")
}

function applyManagedPresetTheme(
  lightCssVariables: GalleryThemeCssVariables,
  darkCssVariables: GalleryThemeCssVariables
) {
  const styleElement = getManagedThemeStyleElement()
  const lightVariables = formatCssVariables(lightCssVariables)
  const darkVariables = formatCssVariables(darkCssVariables)

  styleElement.textContent = `:root {\n${lightVariables}\n}\n\n.dark {\n${darkVariables}\n}\n`
}

function clearManagedPresetTheme() {
  const styleElement = document.getElementById(galleryAppliedThemeStyleId)
  styleElement?.remove()
}

function parseTokenThemeDraft(value: unknown): GalleryTokenThemeDraft | null {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return null
  }

  const parsedValue = value as Partial<
    Record<GalleryColorTokenName, Partial<{ family: string; step: string }>>
  >
  const nextValues = { ...galleryColorTokenDefaults }

  for (const tokenName of Object.keys(
    galleryColorTokenDefaults
  ) as GalleryColorTokenName[]) {
    const token = parsedValue[tokenName]
    if (
      !token ||
      !galleryColorFamilySet.has(token.family ?? "") ||
      !galleryColorStepSet.has(token.step ?? "")
    ) {
      return null
    }

    nextValues[tokenName] = {
      family: token.family as GalleryColorFamily,
      step: token.step as GalleryColorStep,
    }
  }

  return {
    colorTokenValues: nextValues,
    kind: "tokens",
  }
}

function parseThemeDraft(value: unknown): GalleryThemeDraft | null {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return null
  }

  const draft = value as Partial<{
    colorTokenValues: unknown
    darkCssVariables: unknown
    id: unknown
    kind: unknown
    lightCssVariables: unknown
  }>

  if (draft.kind === "tokens") {
    return parseTokenThemeDraft(draft.colorTokenValues)
  }

  if (
    draft.kind === "preset" &&
    typeof draft.id === "string" &&
    galleryThemePresets.some((preset) => preset.id === draft.id) &&
    isCssVariables(draft.lightCssVariables) &&
    isCssVariables(draft.darkCssVariables)
  ) {
    return {
      darkCssVariables: draft.darkCssVariables,
      id: draft.id as GalleryThemePresetId,
      kind: "preset",
      lightCssVariables: draft.lightCssVariables,
    }
  }

  return parseTokenThemeDraft(value)
}

export function createDefaultGalleryThemeDraft(): GalleryThemeDraft {
  return {
    colorTokenValues: galleryColorTokenDefaults,
    kind: "tokens",
  }
}

export function createGalleryPresetThemeDraft(
  presetId: GalleryThemePresetId
): GalleryThemeDraft | null {
  const preset = galleryThemePresets.find((item) => item.id === presetId)

  if (!preset) {
    return null
  }

  if (preset.id === "default") {
    return createDefaultGalleryThemeDraft()
  }

  return {
    darkCssVariables: preset.darkCssVariables ?? preset.lightCssVariables ?? {},
    id: preset.id,
    kind: "preset",
    lightCssVariables: preset.lightCssVariables ?? {},
  }
}

export function resolveGalleryColorTokenCssVariables(
  values: GalleryColorTokenValues
) {
  return Object.fromEntries(
    Object.keys(values).map((tokenName) => [
      `--${tokenName}`,
      resolveTokenColor(values, tokenName as keyof GalleryColorTokenValues),
    ])
  ) as Record<`--${string}`, string>
}

export function createGalleryTokenThemeDraftFromCssVariables(
  cssVariables: GalleryThemeCssVariables
): GalleryTokenThemeDraft {
  const colorTokenValues = { ...galleryColorTokenDefaults }

  for (const tokenName of Object.keys(
    galleryColorTokenDefaults
  ) as GalleryColorTokenName[]) {
    const cssValue = cssVariables[`--${tokenName}`]
    const tokenValue = cssValue ? findNearestTailwindColor(cssValue) : null

    if (tokenValue) {
      colorTokenValues[tokenName] = tokenValue
    }
  }

  return {
    colorTokenValues,
    kind: "tokens",
  }
}

export function resolveGalleryThemeColorTokenValues(
  draft: GalleryThemeDraft,
  resolvedMode: "dark" | "light" = "light"
) {
  if (draft.kind === "tokens") {
    return draft.colorTokenValues
  }

  return createGalleryTokenThemeDraftFromCssVariables(
    resolvedMode === "dark" ? draft.darkCssVariables : draft.lightCssVariables
  ).colorTokenValues
}

export function updateGalleryThemeDraftColorTokenValue({
  draft,
  resolvedMode = "light",
  token,
  value,
}: {
  draft: GalleryThemeDraft
  resolvedMode?: "dark" | "light"
  token: GalleryColorTokenName
  value: GalleryColorTokenValue
}): GalleryThemeDraft {
  if (draft.kind === "tokens") {
    return {
      colorTokenValues: {
        ...draft.colorTokenValues,
        [token]: value,
      },
      kind: "tokens",
    }
  }

  const cssVariableName = `--${token}` as const
  const nextCssVariables = {
    ...(resolvedMode === "dark"
      ? draft.darkCssVariables
      : draft.lightCssVariables),
    [cssVariableName]: resolveTokenValueColor(value),
  }

  return {
    ...draft,
    darkCssVariables:
      resolvedMode === "dark" ? nextCssVariables : draft.darkCssVariables,
    lightCssVariables:
      resolvedMode === "light" ? nextCssVariables : draft.lightCssVariables,
  }
}

export function resolveGalleryThemeCssVariables(
  draft: GalleryThemeDraft,
  resolvedMode: "dark" | "light" = "light"
) {
  if (draft.kind === "preset") {
    return resolvedMode === "dark"
      ? draft.darkCssVariables
      : draft.lightCssVariables
  }

  return resolveGalleryColorTokenCssVariables(draft.colorTokenValues)
}

export function areGalleryThemeDraftsEqual(
  left: GalleryThemeDraft,
  right: GalleryThemeDraft
) {
  return JSON.stringify(left) === JSON.stringify(right)
}

export function applyGalleryTheme(draft: GalleryThemeDraft) {
  const root = document.documentElement

  if (draft.kind === "preset") {
    for (const name of managedRootVariableNames) {
      root.style.removeProperty(name)
    }

    applyManagedPresetTheme(draft.lightCssVariables, draft.darkCssVariables)
    return
  }

  clearManagedPresetTheme()
  const variables = resolveGalleryColorTokenCssVariables(draft.colorTokenValues)

  for (const [name, value] of Object.entries(variables)) {
    root.style.setProperty(name, value)
  }
}

export function loadAppliedGalleryTheme() {
  const storedValue =
    localStorage.getItem(appliedGalleryThemeStorageKey) ??
    localStorage.getItem(legacyAppliedGalleryThemeStorageKey)

  if (!storedValue) {
    return null
  }

  try {
    return parseThemeDraft(JSON.parse(storedValue))
  } catch {
    return null
  }
}

export function saveAppliedGalleryTheme(draft: GalleryThemeDraft) {
  localStorage.setItem(appliedGalleryThemeStorageKey, JSON.stringify(draft))
}
