import { DOCUMENT } from '@angular/common';
import {
  Injectable,
  Signal,
  computed,
  inject,
  signal
} from '@angular/core';

import {
  ThemeAnimationDefinition,
  ThemeContainerKey,
  ThemeDocument,
  ThemeElementStyleProperty,
  ThemeElementStyles
} from '../../domain/models/theme.model';
import {
  BUILT_IN_THEME_PRESETS,
  DEFAULT_THEME_JSON,
  createDefaultThemeDocument,
  createDefaultThemeLayout,
  isLegacyDefaultThemeDocument
} from '../../domain/logic/theme-defaults.logic';
import { createAnimationStarterDefinition } from '../../domain/logic/theme-schema.logic';
import { findThemeLayoutContainer } from '../../domain/logic/theme-registry.logic';
import { validateThemeDocument } from '../../domain/logic/theme-validation.logic';
import { AppI18nService } from '../../../../core/i18n';
import {
  loadThemeStorageSnapshot,
  saveActiveThemeText,
  saveDraftThemeText
} from '../../infrastructure/util/theme-storage.util';

function toKebabCase(value: string): string {
  return value
    .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
    .replace(/[_\s]+/g, '-')
    .toLowerCase();
}

function isEmptyRecord(value: Record<string, unknown>): boolean {
  return Object.keys(value).length === 0;
}

function hasTokenOverrides(document: ThemeDocument): boolean {
  return (
    !isEmptyRecord(document.tokens.colors) ||
    !isEmptyRecord(document.tokens.spacing) ||
    !isEmptyRecord(document.tokens.radii) ||
    !isEmptyRecord(document.tokens.effects)
  );
}

function compactThemeElements(elements: ThemeDocument['elements']): ThemeDocument['elements'] {
  return Object.fromEntries(Object.entries(elements).filter(([_key, styles]) => !isEmptyRecord(styles as Record<string, unknown>)));
}

function stringifyTheme(document: ThemeDocument): string {
  const jsonDocument: Partial<ThemeDocument> = {
    meta: document.meta
  };
  const compactElements = compactThemeElements(document.elements);

  if (document.css.trim().length > 0) {
    jsonDocument.css = document.css;
  }

  if (hasTokenOverrides(document)) {
    jsonDocument.tokens = document.tokens;
  }

  if (!isEmptyRecord(document.layout)) {
    jsonDocument.layout = document.layout;
  }

  if (!isEmptyRecord(compactElements)) {
    jsonDocument.elements = compactElements;
  }

  if (!isEmptyRecord(document.animations)) {
    jsonDocument.animations = document.animations;
  }

  return JSON.stringify(jsonDocument, null, 2);
}

function looksLikeImageReference(value: string): boolean {
  return value.startsWith('http://') || value.startsWith('https://') || value.startsWith('/') || value.startsWith('./') || value.startsWith('../');
}

function normalizeBackgroundImageLayer(value: string): string {
  const trimmedValue = value.trim();

  if (!looksLikeImageReference(trimmedValue)) {
    return trimmedValue;
  }

  return `url("${trimmedValue.replace(/"/g, '\\"')}")`;
}

function resolveBuiltInDefaultMigration(document: ThemeDocument): ThemeDocument {
  return isLegacyDefaultThemeDocument(document) ? createDefaultThemeDocument() : document;
}

const hostStylePropertyKeys = [
  'width',
  'height',
  'minWidth',
  'minHeight',
  'maxWidth',
  'maxHeight',
  'position',
  'top',
  'right',
  'bottom',
  'left',
  'padding',
  'margin',
  'border',
  'borderRadius',
  'backgroundColor',
  'color',
  'backgroundSize',
  'backgroundPosition',
  'backgroundRepeat',
  'boxShadow',
  'backdropFilter'
] as const satisfies readonly (keyof ThemeElementStyles)[];

@Injectable({ providedIn: 'root' })
export class ThemeService {
  readonly activeTheme: Signal<ThemeDocument>;
  readonly activeThemeText: Signal<string>;
  readonly draftTheme: Signal<ThemeDocument>;
  readonly draftText: Signal<string>;
  readonly draftIsValid: Signal<boolean>;
  readonly draftErrors: Signal<string[]>;
  readonly statusMessage: Signal<string | null>;
  readonly activeThemeName: Signal<string>;
  readonly knownAnimationClasses: Signal<string[]>;
  readonly isDraftDirty: Signal<boolean>;
  readonly builtInPresets = BUILT_IN_THEME_PRESETS;

  private readonly documentRef = inject(DOCUMENT);
  private readonly appI18n = inject(AppI18nService);

  private readonly activeThemeInternal = signal<ThemeDocument>(createDefaultThemeDocument());
  private readonly activeThemeTextInternal = signal(DEFAULT_THEME_JSON);
  private readonly draftThemeInternal = signal<ThemeDocument>(createDefaultThemeDocument());
  private readonly draftTextInternal = signal(DEFAULT_THEME_JSON);
  private readonly draftIsValidInternal = signal(true);
  private readonly draftErrorsInternal = signal<string[]>([]);
  private readonly statusMessageInternal = signal<string | null>(null);

  private initialized = false;
  private statusTimeoutId: ReturnType<typeof setTimeout> | null = null;
  private animationStyleElement: HTMLStyleElement | null = null;
  private cssStyleElement: HTMLStyleElement | null = null;

  constructor() {
    this.activeTheme = this.activeThemeInternal.asReadonly();
    this.activeThemeText = this.activeThemeTextInternal.asReadonly();
    this.draftTheme = this.draftThemeInternal.asReadonly();
    this.draftText = this.draftTextInternal.asReadonly();
    this.draftIsValid = this.draftIsValidInternal.asReadonly();
    this.draftErrors = this.draftErrorsInternal.asReadonly();
    this.statusMessage = this.statusMessageInternal.asReadonly();
    this.activeThemeName = computed(() => this.activeThemeInternal().meta.name);
    this.knownAnimationClasses = computed(() => Object.keys(this.draftThemeInternal().animations));
    this.isDraftDirty = computed(() => {
      return this.draftTextInternal().trim() !== this.activeThemeTextInternal().trim();
    });
  }

  initialize(): void {
    if (this.initialized) {
      return;
    }

    this.initialized = true;

    const storageSnapshot = loadThemeStorageSnapshot();
    const activeText = storageSnapshot.activeText ?? DEFAULT_THEME_JSON;
    const activeResult = this.parseAndValidateTheme(activeText, 'saved active theme');

    if (activeResult.valid && activeResult.value) {
      const resolvedTheme = resolveBuiltInDefaultMigration(activeResult.value);
      const formatted = stringifyTheme(resolvedTheme);

      this.activeThemeInternal.set(resolvedTheme);
      this.activeThemeTextInternal.set(formatted);
      saveActiveThemeText(formatted);
    } else {
      const defaultTheme = createDefaultThemeDocument();
      const defaultText = stringifyTheme(defaultTheme);

      this.activeThemeInternal.set(defaultTheme);
      this.activeThemeTextInternal.set(defaultText);
      saveActiveThemeText(defaultText);
    }

    const draftText = storageSnapshot.draftText ?? this.activeThemeTextInternal();
    const draftResult = this.parseAndValidateTheme(draftText, 'saved draft theme');

    if (draftResult.valid && draftResult.value) {
      const resolvedDraftTheme = resolveBuiltInDefaultMigration(draftResult.value);
      const formattedDraft = stringifyTheme(resolvedDraftTheme);

      this.draftThemeInternal.set(resolvedDraftTheme);
      this.draftTextInternal.set(formattedDraft);
      this.draftIsValidInternal.set(true);
      this.draftErrorsInternal.set([]);
      saveDraftThemeText(formattedDraft);
    } else {
      this.draftThemeInternal.set(this.activeThemeInternal());
      this.draftTextInternal.set(this.activeThemeTextInternal());
      this.draftIsValidInternal.set(false);
      this.draftErrorsInternal.set(draftResult.errors);
    }

    this.syncAnimationStylesheet();
    this.syncCssStylesheet();
  }

  updateDraftText(text: string): void {
    this.draftTextInternal.set(text);
    saveDraftThemeText(text);

    const result = this.parseAndValidateTheme(text, 'theme draft');

    if (result.valid && result.value) {
      this.draftThemeInternal.set(result.value);
      this.draftIsValidInternal.set(true);
      this.draftErrorsInternal.set([]);
      return;
    }

    this.draftIsValidInternal.set(false);
    this.draftErrorsInternal.set(result.errors);
  }

  formatDraft(): void {
    if (!this.draftIsValidInternal()) {
      this.setStatusMessage(this.appI18n.instant('theme.status.fixJsonBeforeFormat'));
      return;
    }

    const formatted = stringifyTheme(this.draftThemeInternal());

    this.draftTextInternal.set(formatted);
    saveDraftThemeText(formatted);
    this.setStatusMessage(this.appI18n.instant('theme.status.draftFormatted'));
  }

  applyDraft(): boolean {
    if (!this.draftIsValidInternal()) {
      this.setStatusMessage(this.appI18n.instant('theme.status.draftValidationErrors'));
      return false;
    }

    const formatted = stringifyTheme(this.draftThemeInternal());

    this.commitTheme(this.draftThemeInternal(), formatted, this.appI18n.instant('theme.status.themeApplied'));
    return true;
  }

  buildDraftTextWithCss(css: string): string | null {
    const theme = this.composeDraftThemeWithCss(css);

    return theme ? stringifyTheme(theme) : null;
  }

  applyCssOnlyTheme(css: string): boolean {
    const theme = this.composeDraftThemeWithCss(css);

    if (!theme) {
      return false;
    }

    const formatted = stringifyTheme(theme);

    this.commitTheme(theme, formatted, this.appI18n.instant('theme.status.cssApplied'));
    return true;
  }

  loadThemeText(text: string, mode: 'draft' | 'apply', successMessage: string, sourceLabel = 'theme'): boolean {
    const result = this.parseAndValidateTheme(text, sourceLabel);

    if (!result.valid || !result.value) {
      this.setStatusMessage(result.errors[0] ?? this.appI18n.instant('theme.status.couldNotLoad', { source: sourceLabel }));
      return false;
    }

    const resolvedTheme = resolveBuiltInDefaultMigration(result.value);
    const formatted = stringifyTheme(resolvedTheme);

    if (mode === 'apply') {
      this.commitTheme(resolvedTheme, formatted, successMessage);
      return true;
    }

    this.draftThemeInternal.set(resolvedTheme);
    this.draftTextInternal.set(formatted);
    this.draftIsValidInternal.set(true);
    this.draftErrorsInternal.set([]);
    saveDraftThemeText(formatted);
    this.setStatusMessage(successMessage);
    return true;
  }

  announceStatus(message: string): void {
    this.setStatusMessage(message);
  }

  resetToDefault(reason: 'button' | 'shortcut' = 'button'): void {
    const defaultTheme = createDefaultThemeDocument();
    const defaultText = stringifyTheme(defaultTheme);

    this.activeThemeInternal.set(defaultTheme);
    this.activeThemeTextInternal.set(defaultText);
    this.draftThemeInternal.set(defaultTheme);
    this.draftTextInternal.set(defaultText);
    this.draftIsValidInternal.set(true);
    this.draftErrorsInternal.set([]);
    saveActiveThemeText(defaultText);
    saveDraftThemeText(defaultText);
    this.syncAnimationStylesheet();
    this.syncCssStylesheet();
    this.setStatusMessage(reason === 'shortcut'
      ? this.appI18n.instant('theme.status.resetByShortcut')
      : this.appI18n.instant('theme.status.resetByButton'));
  }

  applyBuiltInPreset(name: string): boolean {
    const preset = this.builtInPresets.find((builtInPreset) => builtInPreset.theme.meta.name === name || builtInPreset.key === name);

    if (!preset) {
      this.setStatusMessage(this.appI18n.instant('theme.status.presetNotFound'));
      return false;
    }

    const theme = structuredClone(preset.theme);
    const result = validateThemeDocument(theme);

    if (!result.valid || !result.value) {
      this.setStatusMessage(result.errors[0] ?? this.appI18n.instant('theme.status.presetCouldNotApply'));
      return false;
    }

    this.commitTheme(
      result.value,
      stringifyTheme(result.value),
      this.appI18n.instant('theme.status.presetApplied', { name: result.value.meta.name })
    );

    return true;
  }

  handleGlobalShortcut(event: KeyboardEvent): boolean {
    const usesModifier = event.ctrlKey || event.metaKey;

    if (!usesModifier || !event.shiftKey || event.code !== 'Digit0') {
      return false;
    }

    event.preventDefault();
    this.resetToDefault('shortcut');
    return true;
  }

  ensureElementEntry(key: string): void {
    if (!this.draftIsValidInternal()) {
      this.setStatusMessage(this.appI18n.instant('theme.status.fixJsonBeforeTools'));
      return;
    }

    const draftJson = JSON.parse(this.draftTextInternal()) as Record<string, unknown>;
    const existingElements = draftJson['elements'];
    const elements =
      existingElements && typeof existingElements === 'object' && !Array.isArray(existingElements)
        ? { ...(existingElements as Record<string, unknown>) }
        : {};

    elements[key] = elements[key] ?? {};
    draftJson['elements'] = elements;

    const result = validateThemeDocument(draftJson);

    if (!result.valid || !result.value) {
      this.setStatusMessage(this.appI18n.instant('theme.status.structuredChangeInvalid'));
      return;
    }

    const formatted = JSON.stringify(draftJson, null, 2);

    this.draftThemeInternal.set(result.value);
    this.draftTextInternal.set(formatted);
    this.draftIsValidInternal.set(true);
    this.draftErrorsInternal.set([]);
    saveDraftThemeText(formatted);
    this.setStatusMessage(this.appI18n.instant('theme.status.preparedElement', { key }));
  }

  ensureLayoutEntry(key: string): void {
    this.updateStructuredDraft(
      (draft) => {
        const defaults = createDefaultThemeLayout();

        draft.layout[key] = draft.layout[key] ?? defaults[key];
      },
      false,
      this.appI18n.instant('theme.status.preparedLayout', { key })
    );
  }

  setElementStyle(key: string, property: ThemeElementStyleProperty, value: string | number, applyImmediately = true): void {
    this.updateStructuredDraft(
      (draft) => {
        draft.elements[key] = {
          ...draft.elements[key],
          [property]: value
        };
      },
      applyImmediately,
      this.appI18n.instant('theme.status.elementUpdated', { key })
    );
  }

  setAnimation(key: string, definition: ThemeAnimationDefinition = createAnimationStarterDefinition(), applyImmediately = true): void {
    this.updateStructuredDraft(
      (draft) => {
        draft.animations[key] = definition;
      },
      applyImmediately,
      this.appI18n.instant('theme.status.animationUpdated', { key })
    );
  }

  getHostStyles(key: string): Record<string, string> {
    const elementTheme = this.activeThemeInternal().elements[key] ?? {};
    const styles: Record<string, string> = {};

    if (key === 'appRoot') {
      Object.assign(styles, this.buildTokenStyles(this.activeThemeInternal()));
    }

    const backgroundLayers = [elementTheme.gradient, elementTheme.backgroundImage]
      .filter((layer): layer is string => typeof layer === 'string' && layer.trim().length > 0)
      .map((layer) => normalizeBackgroundImageLayer(layer));

    if (backgroundLayers.length > 0) {
      styles['backgroundImage'] = backgroundLayers.join(', ');
    }

    for (const property of hostStylePropertyKeys) {
      const value = elementTheme[property];

      if (value) {
        styles[property] = value;
      }
    }

    if (typeof elementTheme.opacity === 'number') {
      styles['opacity'] = `${elementTheme.opacity}`;
    }

    return styles;
  }

  getAnimationClass(key: string): string | null {
    const animationClass = this.activeThemeInternal().elements[key]?.animationClass?.trim();

    return animationClass && animationClass.length > 0 ? animationClass : null;
  }

  getLink(key: string): string | null {
    return this.activeThemeInternal().elements[key]?.link ?? null;
  }

  getTextOverride(key: string): string | null {
    return this.activeThemeInternal().elements[key]?.textOverride ?? null;
  }

  getIcon(key: string): string | null {
    return this.activeThemeInternal().elements[key]?.icon ?? null;
  }

  getLayoutContainerStyles(containerKey: ThemeContainerKey): Record<string, string> {
    const container = findThemeLayoutContainer(containerKey);

    if (!container) {
      return {
        display: 'grid'
      };
    }

    return {
      display: 'grid',
      gridTemplateColumns: container.templateColumns ?? `repeat(${container.columns}, minmax(0, 1fr))`,
      gridTemplateRows: container.templateRows ?? (container.rows === 1 ? 'minmax(0, 1fr)' : `repeat(${container.rows}, minmax(0, 1fr))`),
      minHeight: '0',
      minWidth: '0'
    };
  }

  getLayoutItemStyles(key: string): Record<string, string> {
    const defaults = createDefaultThemeLayout();
    const layoutEntry = this.activeThemeInternal().layout[key] ?? defaults[key];

    if (!layoutEntry) {
      return {};
    }

    return {
      gridColumn: `${layoutEntry.grid.x + 1} / span ${layoutEntry.grid.w}`,
      gridRow: `${layoutEntry.grid.y + 1} / span ${layoutEntry.grid.h}`,
      minWidth: '0',
      minHeight: '0'
    };
  }

  updateStructuredDraft(mutator: (draft: ThemeDocument) => void, applyImmediately: boolean, successMessage: string): void {
    if (!this.draftIsValidInternal()) {
      this.setStatusMessage(this.appI18n.instant('theme.status.fixJsonBeforeTools'));
      return;
    }

    const nextDraft = structuredClone(this.draftThemeInternal());

    mutator(nextDraft);

    const result = validateThemeDocument(nextDraft);

    if (!result.valid || !result.value) {
      this.setStatusMessage(this.appI18n.instant('theme.status.structuredChangeInvalid'));
      return;
    }

    const formatted = stringifyTheme(result.value);

    this.draftThemeInternal.set(result.value);
    this.draftTextInternal.set(formatted);
    this.draftIsValidInternal.set(true);
    this.draftErrorsInternal.set([]);
    saveDraftThemeText(formatted);

    if (applyImmediately) {
      this.commitTheme(result.value, formatted, successMessage);
      return;
    }

    this.setStatusMessage(successMessage);
  }

  private commitTheme(theme: ThemeDocument, text: string, successMessage: string): void {
    this.activeThemeInternal.set(theme);
    this.activeThemeTextInternal.set(text);
    this.draftThemeInternal.set(theme);
    this.draftTextInternal.set(text);
    this.draftIsValidInternal.set(true);
    this.draftErrorsInternal.set([]);
    saveActiveThemeText(text);
    saveDraftThemeText(text);
    this.syncAnimationStylesheet();
    this.syncCssStylesheet();
    this.setStatusMessage(successMessage);
  }

  private composeDraftThemeWithCss(css: string): ThemeDocument | null {
    if (!this.draftIsValidInternal()) {
      this.setStatusMessage(this.appI18n.instant('theme.status.fixJsonBeforeCss'));
      return null;
    }

    const theme = {
      ...structuredClone(this.draftThemeInternal()),
      css
    };
    const result = validateThemeDocument(theme);

    if (!result.valid || !result.value) {
      this.setStatusMessage(result.errors[0] ?? this.appI18n.instant('theme.status.cssOnlyCouldNotApply'));
      return null;
    }

    return result.value;
  }

  private parseAndValidateTheme(text: string, label: string) {
    try {
      return validateThemeDocument(JSON.parse(text) as unknown);
    } catch (error) {
      return {
        valid: false,
        errors: [`${label} could not be parsed: ${error instanceof Error ? error.message : 'unknown JSON error'}`],
        value: null
      };
    }
  }

  private buildTokenStyles(theme: ThemeDocument): Record<string, string> {
    const styles: Record<string, string> = {};

    for (const [tokenName, tokenValue] of Object.entries(theme.tokens.colors)) {
      styles[`--${toKebabCase(tokenName)}`] = tokenValue;
    }

    for (const [tokenName, tokenValue] of Object.entries(theme.tokens.spacing)) {
      styles[`--theme-spacing-${toKebabCase(tokenName)}`] = tokenValue;
    }

    for (const [tokenName, tokenValue] of Object.entries(theme.tokens.radii)) {
      const cssVariableName = tokenName === 'radius' ? '--radius' : `--theme-radius-${toKebabCase(tokenName)}`;

      styles[cssVariableName] = tokenValue;
    }

    for (const [tokenName, tokenValue] of Object.entries(theme.tokens.effects)) {
      styles[`--theme-effect-${toKebabCase(tokenName)}`] = tokenValue;
    }

    return styles;
  }

  private syncAnimationStylesheet(): void {
    const theme = this.activeThemeInternal();
    const css = Object.entries(theme.animations)
      .map(([className, definition]) => this.buildAnimationRule(className, definition))
      .filter((rule) => rule.length > 0)
      .join('\n\n');

    if (!this.animationStyleElement) {
      this.animationStyleElement = this.documentRef.createElement('style');
      this.animationStyleElement.setAttribute('data-toju-theme-animations', 'true');
      this.documentRef.head.appendChild(this.animationStyleElement);
    }

    this.animationStyleElement.textContent = css;
  }

  private syncCssStylesheet(): void {
    const css = this.activeThemeInternal().css.trim();

    if (!this.cssStyleElement) {
      this.cssStyleElement = this.documentRef.createElement('style');
      this.cssStyleElement.setAttribute('data-toju-theme-css', 'true');
      this.documentRef.head.appendChild(this.cssStyleElement);
    }

    this.cssStyleElement.textContent = css;
  }

  private buildAnimationRule(className: string, definition: ThemeAnimationDefinition): string {
    const animationClass = `.${className}`;
    const declarationLines = [
      `animation-name: ${className};`,
      `animation-duration: ${definition.duration ?? '240ms'};`,
      `animation-timing-function: ${definition.easing ?? 'ease'};`,
      `animation-delay: ${definition.delay ?? '0ms'};`,
      `animation-iteration-count: ${definition.iterationCount ?? '1'};`,
      `animation-fill-mode: ${definition.fillMode ?? 'both'};`,
      `animation-direction: ${definition.direction ?? 'normal'};`
    ];
    const classRule = `${animationClass} {\n  ${declarationLines.join('\n  ')}\n}`;

    if (!definition.keyframes || Object.keys(definition.keyframes).length === 0) {
      return classRule;
    }

    const keyframeRule = `@keyframes ${className} {\n${Object.entries(definition.keyframes)
      .map(([step, declarations]) => {
        const lines = Object.entries(declarations)
          .map(([property, value]) => `    ${toKebabCase(property)}: ${value};`)
          .join('\n');

        return `  ${step} {\n${lines}\n  }`;
      })
      .join('\n')}\n}`;

    return `${keyframeRule}\n\n${classRule}`;
  }

  private setStatusMessage(message: string): void {
    this.statusMessageInternal.set(message);

    if (this.statusTimeoutId) {
      clearTimeout(this.statusTimeoutId);
    }

    this.statusTimeoutId = setTimeout(() => {
      this.statusMessageInternal.set(null);
      this.statusTimeoutId = null;
    }, 5000);
  }
}
