'use client'

import {
  classifyLogoShape,
  logoShapeWarning,
  resolveAdminBrand,
  resolvePublicBrand,
  validateAccentColor,
  type AdminBrandingMode,
  type BrandAssetLike,
  type BrandAssetRole,
  type BrandSettings,
  type ColorMode,
  type LogoShape,
  type ResolvedAdminBrand,
  type ResolvedPublicBrand,
  type SidebarDensity,
} from '@actuate-media/cms-core/appearance'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'

import { useTheme, type AdminBrandCache } from '../../components/ThemeProvider.js'
import { cmsApi } from '../../lib/api.js'
import { BRAND_UPDATED_EVENT } from '../../lib/favicon.js'
import type { SettingsSourceMetadata, ValidationIssue } from './useGeneralSettings.js'

export type SaveState = 'idle' | 'saving' | 'success' | 'error'

/** Theme + typography + motion form (the `appearance` settings blob). */
export interface AppearanceForm {
  colorMode: ColorMode
  accent: string
  density: SidebarDensity
  font: string
  reduceMotion: boolean
}

/** Brand asset references + admin branding mode (the `brand` settings blob). */
export interface BrandForm {
  mode: AdminBrandingMode
  publicPrimaryLogoAssetId?: string
  publicLogoMarkAssetId?: string
  publicDarkLogoAssetId?: string
  adminPrimaryLogoAssetId?: string
  adminLogoMarkAssetId?: string
  adminDarkLogoAssetId?: string
  faviconSourceAssetId?: string
  defaultLogoAltText?: string
}

/** A brand-form field that points at a media asset, plus its asset role. */
export type BrandAssetField =
  | 'publicPrimaryLogoAssetId'
  | 'publicLogoMarkAssetId'
  | 'publicDarkLogoAssetId'
  | 'adminPrimaryLogoAssetId'
  | 'adminLogoMarkAssetId'
  | 'adminDarkLogoAssetId'
  | 'faviconSourceAssetId'

export const FIELD_ROLE: Record<BrandAssetField, BrandAssetRole> = {
  publicPrimaryLogoAssetId: 'publicPrimaryLogo',
  publicLogoMarkAssetId: 'publicLogoMark',
  publicDarkLogoAssetId: 'publicDarkLogo',
  adminPrimaryLogoAssetId: 'adminPrimaryLogo',
  adminLogoMarkAssetId: 'adminLogoMark',
  adminDarkLogoAssetId: 'adminDarkLogo',
  faviconSourceAssetId: 'faviconSource',
}

export interface AssetPreview {
  id: string
  url: string
  mimeType: string
  width?: number
  height?: number
  shape: LogoShape
}

/** An existing media-library asset chosen for a brand field via the picker. */
export interface SelectedBrandAsset {
  id: string
  url: string
  mimeType: string
  width?: number
  height?: number
}

/**
 * Formats accepted for brand logos. Everything here is also validated
 * server-side (allowlist + magic bytes) in cms-core's upload security.
 * Brand uploads are stored as-is (`skipOptimize`), so each format keeps
 * its native fidelity — including WebP, which general media uploads are
 * converted to anyway.
 */
export const LOGO_MIME = [
  'image/svg+xml',
  'image/png',
  'image/webp',
  'image/jpeg',
  'image/gif',
  'image/avif',
]
/** Favicons need crisp edges at small sizes — vector or lossless/modern raster only. */
export const FAVICON_MIME = ['image/svg+xml', 'image/png', 'image/webp']
const MAX_BYTES = 1024 * 1024 // 1 MB

const MIME_LABEL: Record<string, string> = {
  'image/svg+xml': 'SVG',
  'image/png': 'PNG',
  'image/webp': 'WebP',
  'image/jpeg': 'JPEG',
  'image/gif': 'GIF',
  'image/avif': 'AVIF',
}

function formatAllowed(mimes: string[]): string {
  return mimes.map((m) => MIME_LABEL[m] ?? m).join(', ')
}

export interface UseAppearanceSettings {
  loading: boolean
  loadError: string | null
  appearance: AppearanceForm
  brand: BrandForm
  /** Per-asset-id preview metadata (loaded + freshly uploaded). */
  assets: Record<string, AssetPreview>
  sources: Record<string, SettingsSourceMetadata>
  issues: ValidationIssue[]
  preview: { admin: ResolvedAdminBrand; public: ResolvedPublicBrand; faviconUrl: string | null }
  dirty: boolean
  saveState: SaveState
  saveError: string | null
  uploadingField: BrandAssetField | null
  showAdvanced: boolean
  setShowAdvanced: (v: boolean) => void
  setColorMode: (v: ColorMode) => void
  setAccent: (v: string) => void
  setDensity: (v: SidebarDensity) => void
  setFont: (v: string) => void
  setReduceMotion: (v: boolean) => void
  setBrandingMode: (v: AdminBrandingMode) => void
  setAltText: (v: string) => void
  uploadAsset: (field: BrandAssetField, file: File) => Promise<void>
  /** Point a brand field at an EXISTING media asset (no re-upload, no duplicate). */
  selectAsset: (field: BrandAssetField, item: SelectedBrandAsset) => void
  removeAsset: (field: BrandAssetField) => void
  save: () => Promise<void>
  reset: () => void
  reload: () => void
}

const DEFAULT_FORM: AppearanceForm = {
  colorMode: 'system',
  accent: 'purple',
  density: 'default',
  font: 'DM Sans',
  reduceMotion: false,
}

const DEFAULT_BRAND_FORM: BrandForm = { mode: 'inheritPublicBrand' }

function snapshot(a: AppearanceForm, b: BrandForm): string {
  return JSON.stringify({ a, b })
}

interface AppearanceApiResponse {
  appearance: {
    adminColorModeDefault: ColorMode
    adminAccentColor: string
    sidebarDensity: SidebarDensity
    contentFont: string
    reduceMotion: boolean
  }
  brand: BrandSettings
  resolved: { admin: { initials: string } }
  assets: BrandAssetLike[]
  siteTitle: string | null
}

/** Read an uploaded image's shape client-side (raster via Image, SVG via XML). */
async function detectShape(file: File): Promise<LogoShape> {
  if (file.type === 'image/svg+xml') {
    try {
      const text = await file.text()
      const viewBox = text.match(/viewBox\s*=\s*["']\s*[\d.]+\s+[\d.]+\s+([\d.]+)\s+([\d.]+)/i)
      if (viewBox) return classifyLogoShape(parseFloat(viewBox[1]!), parseFloat(viewBox[2]!))
      const w = text.match(/\bwidth\s*=\s*["']([\d.]+)/i)
      const h = text.match(/\bheight\s*=\s*["']([\d.]+)/i)
      if (w && h) return classifyLogoShape(parseFloat(w[1]!), parseFloat(h[1]!))
    } catch {
      /* fall through */
    }
    return 'unknown'
  }
  // Shape detection only powers advisory warnings — it must never block or
  // fail an upload, so any decode problem resolves to 'unknown'.
  try {
    return await new Promise<LogoShape>((resolve) => {
      const url = URL.createObjectURL(file)
      const img = new Image()
      img.onload = () => {
        URL.revokeObjectURL(url)
        resolve(classifyLogoShape(img.naturalWidth, img.naturalHeight))
      }
      img.onerror = () => {
        URL.revokeObjectURL(url)
        resolve('unknown')
      }
      img.src = url
    })
  } catch {
    return 'unknown'
  }
}

function assetToCache(
  brand: BrandForm,
  assets: Record<string, AssetPreview>,
  siteTitle: string | null,
): AdminBrandCache {
  const map = new Map<string, BrandAssetLike>(
    Object.values(assets).map((a) => [
      a.id,
      { id: a.id, publicUrl: a.url, mimeType: a.mimeType, width: a.width, height: a.height },
    ]),
  )
  const resolved = resolveAdminBrand(brand as BrandSettings, map, siteTitle)
  return {
    primaryUrl: resolved.primary?.url ?? null,
    markUrl: resolved.mark?.url ?? null,
    darkUrl: resolved.dark?.url ?? null,
    initials: resolved.initials,
  }
}

export function useAppearanceSettings(
  canEdit: boolean,
  config?: Record<string, unknown>,
): UseAppearanceSettings {
  const { applyAppearance, setAdminBrand } = useTheme()

  const [loading, setLoading] = useState(true)
  const [loadError, setLoadError] = useState<string | null>(null)
  const [appearance, setAppearance] = useState<AppearanceForm>(DEFAULT_FORM)
  const [brand, setBrand] = useState<BrandForm>(DEFAULT_BRAND_FORM)
  const [assets, setAssets] = useState<Record<string, AssetPreview>>({})
  const [saveState, setSaveState] = useState<SaveState>('idle')
  const [saveError, setSaveError] = useState<string | null>(null)
  const [uploadingField, setUploadingField] = useState<BrandAssetField | null>(null)
  const [showAdvanced, setShowAdvanced] = useState(false)

  const baselineRef = useRef<string>(snapshot(DEFAULT_FORM, DEFAULT_BRAND_FORM))
  const siteTitleRef = useRef<string | null>(null)

  // Config-managed source detection (admin branding configured in actuate.config.ts).
  const branding = (config?.admin as Record<string, unknown> | undefined)?.branding as
    | Record<string, unknown>
    | undefined
  const sources = useMemo<Record<string, SettingsSourceMetadata>>(() => {
    const out: Record<string, SettingsSourceMetadata> = {}
    if (branding?.primaryColor) {
      out.accent = {
        source: 'config',
        editable: false,
        reason: 'Primary color is set in actuate.config.ts (admin.branding.primaryColor).',
      }
    }
    if (branding?.darkMode !== undefined) {
      out.colorMode = {
        source: 'config',
        editable: true,
        reason: 'Default dark mode comes from admin.branding.darkMode; you can still override it.',
      }
    }
    return out
  }, [branding])

  const pushLive = useCallback(
    (a: AppearanceForm, b: BrandForm, assetMap: Record<string, AssetPreview>) => {
      applyAppearance({
        theme: a.colorMode,
        accent: a.accent,
        density: a.density,
        font: a.font,
        reduceMotion: a.reduceMotion,
        adminBrand: assetToCache(b, assetMap, siteTitleRef.current),
      })
    },
    [applyAppearance],
  )

  const load = useCallback(async () => {
    setLoading(true)
    setLoadError(null)
    const res = await cmsApi<AppearanceApiResponse>('/appearance')
    if (res.error || !res.data) {
      setLoadError(res.error ?? 'Failed to load appearance settings')
      setLoading(false)
      return
    }
    const d = res.data
    const nextAppearance: AppearanceForm = {
      colorMode: d.appearance.adminColorModeDefault,
      accent: d.appearance.adminAccentColor,
      density: d.appearance.sidebarDensity,
      font: d.appearance.contentFont,
      reduceMotion: d.appearance.reduceMotion,
    }
    const nextBrand: BrandForm = { ...d.brand, mode: d.brand?.mode ?? 'inheritPublicBrand' }
    const nextAssets: Record<string, AssetPreview> = {}
    for (const a of d.assets ?? []) {
      nextAssets[a.id] = {
        id: a.id,
        url: a.publicUrl,
        mimeType: a.mimeType,
        width: a.width ?? undefined,
        height: a.height ?? undefined,
        shape: classifyLogoShape(a.width ?? undefined, a.height ?? undefined),
      }
    }
    siteTitleRef.current = d.siteTitle ?? null
    setAppearance(nextAppearance)
    setBrand(nextBrand)
    setAssets(nextAssets)
    if (nextBrand.adminLogoMarkAssetId || nextBrand.publicLogoMarkAssetId) setShowAdvanced(true)
    baselineRef.current = snapshot(nextAppearance, nextBrand)
    pushLive(nextAppearance, nextBrand, nextAssets)
    setSaveState('idle')
    setLoading(false)
  }, [pushLive])

  useEffect(() => {
    void load()
  }, [load])

  // ── Mutators (live-apply theme controls before save) ──────────────────────
  const update = useCallback(
    (next: AppearanceForm, b: BrandForm, assetMap: Record<string, AssetPreview>) => {
      setAppearance(next)
      setBrand(b)
      setAssets(assetMap)
      setSaveState('idle')
      pushLive(next, b, assetMap)
    },
    [pushLive],
  )

  const setColorMode = useCallback(
    (v: ColorMode) => update({ ...appearance, colorMode: v }, brand, assets),
    [appearance, brand, assets, update],
  )
  const setAccent = useCallback(
    (v: string) => update({ ...appearance, accent: v }, brand, assets),
    [appearance, brand, assets, update],
  )
  const setDensity = useCallback(
    (v: SidebarDensity) => update({ ...appearance, density: v }, brand, assets),
    [appearance, brand, assets, update],
  )
  const setFont = useCallback(
    (v: string) => update({ ...appearance, font: v }, brand, assets),
    [appearance, brand, assets, update],
  )
  const setReduceMotion = useCallback(
    (v: boolean) => update({ ...appearance, reduceMotion: v }, brand, assets),
    [appearance, brand, assets, update],
  )
  const setBrandingMode = useCallback(
    (v: AdminBrandingMode) => update(appearance, { ...brand, mode: v }, assets),
    [appearance, brand, assets, update],
  )
  const setAltText = useCallback(
    (v: string) => update(appearance, { ...brand, defaultLogoAltText: v }, assets),
    [appearance, brand, assets, update],
  )

  const uploadAsset = useCallback(
    async (field: BrandAssetField, file: File) => {
      const isFavicon = field === 'faviconSourceAssetId'
      const allowed = isFavicon ? FAVICON_MIME : LOGO_MIME
      if (!allowed.includes(file.type)) {
        setSaveError(`Unsupported file type. Allowed: ${formatAllowed(allowed)}.`)
        return
      }
      if (file.size > MAX_BYTES) {
        setSaveError('File exceeds the 1MB limit.')
        return
      }
      setSaveError(null)
      setUploadingField(field)
      try {
        const shape = await detectShape(file)
        const form = new FormData()
        form.append('file', file)
        // Preserve the original format — brand assets must not be transcoded
        // (SVG stays vector, PNG keeps transparency, WebP/AVIF stay as-is).
        form.append('skipOptimize', 'true')
        const res = await cmsApi<{ id: string; url?: string; mimeType?: string }>('/media/upload', {
          method: 'POST',
          body: form,
        })
        if (res.error || !res.data?.id) {
          setSaveError(res.error ?? 'Upload failed')
          return
        }
        const id = res.data.id
        const nextAssets: Record<string, AssetPreview> = {
          ...assets,
          [id]: {
            id,
            url: res.data.url ?? '',
            mimeType: res.data.mimeType ?? file.type,
            shape,
          },
        }
        update(appearance, { ...brand, [field]: id }, nextAssets)
      } finally {
        setUploadingField(null)
      }
    },
    [appearance, brand, assets, update],
  )

  const selectAsset = useCallback(
    (field: BrandAssetField, item: SelectedBrandAsset) => {
      // Same allow-list as upload, regardless of what else lives in the
      // media library (which also holds documents, video, etc.).
      const isFavicon = field === 'faviconSourceAssetId'
      const allowed = isFavicon ? FAVICON_MIME : LOGO_MIME
      if (!allowed.includes(item.mimeType)) {
        setSaveError(
          `Unsupported file type. ${isFavicon ? 'Favicons' : 'Brand assets'} must be ${formatAllowed(allowed)}.`,
        )
        return
      }
      setSaveError(null)
      const nextAssets: Record<string, AssetPreview> = {
        ...assets,
        [item.id]: {
          id: item.id,
          url: item.url,
          mimeType: item.mimeType,
          width: item.width,
          height: item.height,
          shape: classifyLogoShape(item.width, item.height),
        },
      }
      update(appearance, { ...brand, [field]: item.id }, nextAssets)
    },
    [appearance, brand, assets, update],
  )

  const removeAsset = useCallback(
    (field: BrandAssetField) => {
      const next = { ...brand }
      delete next[field]
      update(appearance, next, assets)
    },
    [appearance, brand, assets, update],
  )

  const issues = useMemo<ValidationIssue[]>(() => {
    const out: ValidationIssue[] = []
    const accentCheck = validateAccentColor(appearance.accent)
    if (!accentCheck.valid) {
      out.push({
        field: 'accent',
        message: accentCheck.reason ?? 'Invalid accent color.',
        severity: 'error',
      })
    }
    // Shape warnings per asset field.
    for (const field of Object.keys(FIELD_ROLE) as BrandAssetField[]) {
      const id = brand[field]
      if (!id) continue
      const shape = assets[id]?.shape
      if (!shape) continue
      const w = logoShapeWarning(FIELD_ROLE[field], shape)
      if (w) out.push({ field, message: w, severity: 'warning' })
    }
    // Dark mode default but no dark logo.
    const hasDark = Boolean(brand.publicDarkLogoAssetId || brand.adminDarkLogoAssetId)
    if (appearance.colorMode === 'dark' && !hasDark) {
      out.push({
        field: 'colorMode',
        message:
          'Dark mode is the default but no dark-mode logo is set. The primary logo will be used on dark backgrounds.',
        severity: 'warning',
      })
    }
    return out
  }, [appearance, brand, assets])

  const preview = useMemo(() => {
    const map = new Map<string, BrandAssetLike>(
      Object.values(assets).map((a) => [
        a.id,
        { id: a.id, publicUrl: a.url, mimeType: a.mimeType, width: a.width, height: a.height },
      ]),
    )
    const title = siteTitleRef.current
    const faviconId = brand.faviconSourceAssetId
    return {
      admin: resolveAdminBrand(brand as BrandSettings, map, title),
      public: resolvePublicBrand(brand as BrandSettings, map, title),
      faviconUrl: faviconId ? (assets[faviconId]?.url ?? null) : null,
    }
  }, [brand, assets])

  const dirty = useMemo(
    () => snapshot(appearance, brand) !== baselineRef.current,
    [appearance, brand],
  )

  const save = useCallback(async () => {
    if (!canEdit) return
    if (issues.some((i) => i.severity === 'error')) return
    setSaveState('saving')
    setSaveError(null)

    // Read-modify-write the settings global so we never clobber sibling keys
    // (siteTitle, SEO defaults, AI flags, …) written by other settings tabs.
    const current = await cmsApi<Record<string, unknown>>('/globals/settings')
    if (current.error) {
      setSaveState('error')
      setSaveError(current.error)
      return
    }
    const now = new Date().toISOString()
    const brandPayload: BrandSettings = { ...(brand as BrandSettings), updatedAt: now }
    const appearancePayload = {
      adminColorModeDefault: appearance.colorMode,
      adminAccentColor: appearance.accent,
      sidebarDensity: appearance.density,
      contentFont: appearance.font,
      reduceMotion: appearance.reduceMotion,
      updatedAt: now,
    }
    const merged = {
      ...(current.data ?? {}),
      appearance: appearancePayload,
      brand: brandPayload,
    }
    const res = await cmsApi('/globals/settings', { method: 'PUT', body: JSON.stringify(merged) })
    if (res.error) {
      setSaveState('error')
      setSaveError(res.error)
      return
    }
    baselineRef.current = snapshot(appearance, brand)
    setAdminBrand(assetToCache(brand, assets, siteTitleRef.current))
    // Notify live surfaces (browser-tab favicon) to refresh from the saved
    // brand without a full reload.
    if (typeof window !== 'undefined') {
      window.dispatchEvent(new CustomEvent(BRAND_UPDATED_EVENT))
    }
    setSaveState('success')
  }, [canEdit, issues, appearance, brand, assets, setAdminBrand])

  const reset = useCallback(() => {
    const base = JSON.parse(baselineRef.current) as { a: AppearanceForm; b: BrandForm }
    update(base.a, base.b, assets)
  }, [assets, update])

  return {
    loading,
    loadError,
    appearance,
    brand,
    assets,
    sources,
    issues,
    preview,
    dirty,
    saveState,
    saveError,
    uploadingField,
    showAdvanced,
    setShowAdvanced,
    setColorMode,
    setAccent,
    setDensity,
    setFont,
    setReduceMotion,
    setBrandingMode,
    setAltText,
    uploadAsset,
    selectAsset,
    removeAsset,
    save,
    reset,
    reload: () => void load(),
  }
}
