import {
  ThemeAnimationDefinition,
  ThemeContainerKey,
  ThemeDocument,
  ThemeElementStyleProperty,
  ThemeElementStyles,
  ThemeGridRect,
  ThemeLayoutEntry,
  ThemeValidationResult
} from '../models/theme.model';
import { createDefaultThemeDocument, createDefaultThemeLayout } from './theme-defaults.logic';
import {
  THEME_LAYOUT_CONTAINERS,
  THEME_REGISTRY,
  getLayoutEditableThemeKeys
} from './theme-registry.logic';

const TOP_LEVEL_KEYS = [
  'css',
  'meta',
  'tokens',
  'layout',
  'elements',
  'animations'
] as const;
const META_KEYS = [
  'name',
  'version',
  'description'
] as const;
const TOKEN_GROUP_KEYS = [
  'colors',
  'spacing',
  'radii',
  'effects'
] as const;
const LAYOUT_ENTRY_KEYS = ['container', 'grid'] as const;
const GRID_KEYS = [
  'x',
  'y',
  'w',
  'h'
] as const;
const ANIMATION_KEYS = [
  'duration',
  'easing',
  'delay',
  'iterationCount',
  'fillMode',
  'direction',
  'keyframes'
] as const;
const POSITION_VALUES = [
  'static',
  'relative',
  'absolute',
  'sticky'
] as const;
const FILL_MODE_VALUES = [
  'none',
  'forwards',
  'backwards',
  'both'
] as const;
const DIRECTION_VALUES = [
  'normal',
  'reverse',
  'alternate',
  'alternate-reverse'
] as const;
const SAFE_LINK_PROTOCOLS = ['http:', 'https:'] as const;
const SAFE_CLASS_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*$/;
const KEYFRAME_STEP_PATTERN = /^(from|to|(?:\d|[1-9]\d|100)%)$/;
const ELEMENT_STYLE_KEYS: readonly ThemeElementStyleProperty[] = [
  'width',
  'height',
  'minWidth',
  'minHeight',
  'maxWidth',
  'maxHeight',
  'position',
  'top',
  'right',
  'bottom',
  'left',
  'opacity',
  'padding',
  'margin',
  'border',
  'borderRadius',
  'backgroundColor',
  'color',
  'backgroundImage',
  'backgroundSize',
  'backgroundPosition',
  'backgroundRepeat',
  'gradient',
  'boxShadow',
  'backdropFilter',
  'icon',
  'textOverride',
  'link',
  'animationClass'
];

function isPlainObject(value: unknown): value is Record<string, unknown> {
  return !!value && typeof value === 'object' && !Array.isArray(value);
}

function validateUnknownKeys(value: Record<string, unknown>, allowedKeys: readonly string[], path: string, errors: string[]): void {
  for (const key of Object.keys(value)) {
    if (!allowedKeys.includes(key)) {
      errors.push(`${path}.${key} is not part of the supported theme schema.`);
    }
  }
}

function validateString(value: unknown, path: string, errors: string[], allowEmpty = false): value is string {
  if (typeof value !== 'string') {
    errors.push(`${path} must be a string.`);
    return false;
  }

  if (!allowEmpty && value.trim().length === 0) {
    errors.push(`${path} cannot be empty.`);
    return false;
  }

  return true;
}

function validateEnum<T extends readonly string[]>(value: unknown, allowedValues: T, path: string, errors: string[]): value is T[number] {
  if (typeof value !== 'string' || !allowedValues.includes(value)) {
    errors.push(`${path} must be one of: ${allowedValues.join(', ')}.`);
    return false;
  }

  return true;
}

function validateInteger(value: unknown, path: string, errors: string[], minimum: number, allowZero: boolean): value is number {
  if (typeof value !== 'number' || !Number.isInteger(value)) {
    errors.push(`${path} must be an integer.`);
    return false;
  }

  if ((allowZero && value < minimum) || (!allowZero && value <= minimum)) {
    errors.push(`${path} must be ${allowZero ? 'greater than or equal to' : 'greater than'} ${minimum}.`);
    return false;
  }

  return true;
}

function validateNumberRange(value: unknown, path: string, errors: string[], minimum: number, maximum: number): value is number {
  if (typeof value !== 'number' || Number.isNaN(value)) {
    errors.push(`${path} must be a number.`);
    return false;
  }

  if (value < minimum || value > maximum) {
    errors.push(`${path} must be between ${minimum} and ${maximum}.`);
    return false;
  }

  return true;
}

function validateStringRecord(value: unknown, path: string, errors: string[]): value is Record<string, string> {
  if (!isPlainObject(value)) {
    errors.push(`${path} must be an object containing string values.`);
    return false;
  }

  for (const [key, recordValue] of Object.entries(value)) {
    validateString(recordValue, `${path}.${key}`, errors);
  }

  return true;
}

function validateElementStyles(value: unknown, path: string, errors: string[]): value is ThemeElementStyles {
  if (!isPlainObject(value)) {
    errors.push(`${path} must be an object.`);
    return false;
  }

  validateUnknownKeys(value, ELEMENT_STYLE_KEYS, path, errors);

  for (const [key, fieldValue] of Object.entries(value)) {
    if (key === 'opacity') {
      validateNumberRange(fieldValue, `${path}.${key}`, errors, 0, 1);
      continue;
    }

    if (key === 'position') {
      validateEnum(fieldValue, POSITION_VALUES, `${path}.${key}`, errors);
      continue;
    }

    if (key === 'link') {
      if (validateString(fieldValue, `${path}.${key}`, errors)) {
        try {
          const parsedUrl = new URL(fieldValue);

          if (!SAFE_LINK_PROTOCOLS.includes(parsedUrl.protocol as (typeof SAFE_LINK_PROTOCOLS)[number])) {
            errors.push(`${path}.${key} must use http or https.`);
          }
        } catch {
          errors.push(`${path}.${key} must be a valid absolute URL.`);
        }
      }

      continue;
    }

    if (key === 'animationClass') {
      if (validateString(fieldValue, `${path}.${key}`, errors) && !SAFE_CLASS_PATTERN.test(fieldValue)) {
        errors.push(`${path}.${key} must be a safe CSS class token.`);
      }

      continue;
    }

    validateString(fieldValue, `${path}.${key}`, errors, key === 'backgroundImage' || key === 'gradient');
  }

  return true;
}

function validateAnimationDefinition(value: unknown, path: string, errors: string[]): value is ThemeAnimationDefinition {
  if (!isPlainObject(value)) {
    errors.push(`${path} must be an object.`);
    return false;
  }

  validateUnknownKeys(value, ANIMATION_KEYS, path, errors);

  if (value['duration'] !== undefined) {
    validateString(value['duration'], `${path}.duration`, errors);
  }

  if (value['easing'] !== undefined) {
    validateString(value['easing'], `${path}.easing`, errors);
  }

  if (value['delay'] !== undefined) {
    validateString(value['delay'], `${path}.delay`, errors);
  }

  if (value['iterationCount'] !== undefined) {
    validateString(value['iterationCount'], `${path}.iterationCount`, errors);
  }

  if (value['fillMode'] !== undefined) {
    validateEnum(value['fillMode'], FILL_MODE_VALUES, `${path}.fillMode`, errors);
  }

  if (value['direction'] !== undefined) {
    validateEnum(value['direction'], DIRECTION_VALUES, `${path}.direction`, errors);
  }

  if (value['keyframes'] !== undefined) {
    if (!isPlainObject(value['keyframes'])) {
      errors.push(`${path}.keyframes must be an object.`);
    } else {
      for (const [step, declarations] of Object.entries(value['keyframes'])) {
        if (!KEYFRAME_STEP_PATTERN.test(step)) {
          errors.push(`${path}.keyframes.${step} is not a supported keyframe step.`);
          continue;
        }

        if (!isPlainObject(declarations)) {
          errors.push(`${path}.keyframes.${step} must be an object of CSS declarations.`);
          continue;
        }

        for (const [cssProperty, cssValue] of Object.entries(declarations)) {
          if (typeof cssValue !== 'string' && typeof cssValue !== 'number') {
            errors.push(`${path}.keyframes.${step}.${cssProperty} must be a string or number.`);
          }
        }
      }
    }
  }

  return true;
}

function normaliseThemeDocument(input: Record<string, unknown>): ThemeDocument {
  const defaults = createDefaultThemeDocument();
  const meta = isPlainObject(input['meta']) ? input['meta'] : {};
  const tokens = isPlainObject(input['tokens']) ? input['tokens'] : {};
  const layout = isPlainObject(input['layout']) ? input['layout'] : {};

  return {
    css: typeof input['css'] === 'string' ? input['css'] : '',
    meta: {
      name: typeof meta['name'] === 'string' ? meta['name'] : defaults.meta.name,
      version: typeof meta['version'] === 'string' ? meta['version'] : defaults.meta.version,
      description: typeof meta['description'] === 'string' ? meta['description'] : defaults.meta.description
    },
    tokens: {
      colors: isPlainObject(tokens['colors']) ? (tokens['colors'] as Record<string, string>) : {},
      spacing: isPlainObject(tokens['spacing']) ? (tokens['spacing'] as Record<string, string>) : {},
      radii: isPlainObject(tokens['radii']) ? (tokens['radii'] as Record<string, string>) : {},
      effects: isPlainObject(tokens['effects']) ? (tokens['effects'] as Record<string, string>) : {}
    },
    layout: normaliseThemeLayout(layout, createDefaultThemeLayout()),
    elements: isPlainObject(input['elements']) ? (input['elements'] as Record<string, ThemeElementStyles>) : {},
    animations: isPlainObject(input['animations']) ? (input['animations'] as Record<string, ThemeAnimationDefinition>) : {}
  };
}

function normaliseThemeLayout(input: Record<string, unknown>, defaults: Record<string, ThemeLayoutEntry>): Record<string, ThemeLayoutEntry> {
  const layout: Record<string, ThemeLayoutEntry> = {};

  for (const [key, value] of Object.entries(input)) {
    if (!isPlainObject(value)) {
      continue;
    }

    const defaultEntry = defaults[key];
    const grid = isPlainObject(value['grid']) ? (value['grid'] as Partial<ThemeGridRect>) : {};

    if (!defaultEntry) {
      continue;
    }

    layout[key] = {
      container: typeof value['container'] === 'string' ? (value['container'] as ThemeContainerKey) : defaultEntry.container,
      grid: {
        ...defaultEntry.grid,
        ...grid
      }
    };
  }

  return layout;
}

export function validateThemeDocument(input: unknown): ThemeValidationResult {
  const errors: string[] = [];

  if (!isPlainObject(input)) {
    return {
      valid: false,
      errors: ['Theme document must be a JSON object.'],
      value: null
    };
  }

  validateUnknownKeys(input, TOP_LEVEL_KEYS, 'theme', errors);

  if (input['css'] !== undefined) {
    validateString(input['css'], 'theme.css', errors, true);
  }

  const meta = input['meta'];

  if (meta !== undefined && !isPlainObject(meta)) {
    errors.push('theme.meta must be an object.');
  } else if (isPlainObject(meta)) {
    validateUnknownKeys(meta, META_KEYS, 'theme.meta', errors);

    if (meta['name'] !== undefined) {
      validateString(meta['name'], 'theme.meta.name', errors);
    }

    if (meta['version'] !== undefined) {
      validateString(meta['version'], 'theme.meta.version', errors);
    }

    if (meta['description'] !== undefined) {
      validateString(meta['description'], 'theme.meta.description', errors, true);
    }
  }

  const tokens = input['tokens'];

  if (tokens !== undefined) {
    if (!isPlainObject(tokens)) {
      errors.push('theme.tokens must be an object.');
    } else {
      validateUnknownKeys(tokens, TOKEN_GROUP_KEYS, 'theme.tokens', errors);

      if (tokens['colors'] !== undefined) {
        validateStringRecord(tokens['colors'], 'theme.tokens.colors', errors);
      }

      if (tokens['spacing'] !== undefined) {
        validateStringRecord(tokens['spacing'], 'theme.tokens.spacing', errors);
      }

      if (tokens['radii'] !== undefined) {
        validateStringRecord(tokens['radii'], 'theme.tokens.radii', errors);
      }

      if (tokens['effects'] !== undefined) {
        validateStringRecord(tokens['effects'], 'theme.tokens.effects', errors);
      }
    }
  }

  const layout = input['layout'];

  if (layout !== undefined) {
    if (!isPlainObject(layout)) {
      errors.push('theme.layout must be an object.');
    } else {
      const allowedLayoutKeys = getLayoutEditableThemeKeys();

      validateUnknownKeys(layout, allowedLayoutKeys, 'theme.layout', errors);

      for (const [key, value] of Object.entries(layout)) {
        const basePath = `theme.layout.${key}`;

        if (!isPlainObject(value)) {
          errors.push(`${basePath} must be an object.`);
          continue;
        }

        validateUnknownKeys(value, LAYOUT_ENTRY_KEYS, basePath, errors);

        if (value['container'] !== undefined) {
          validateEnum(
            value['container'],
            THEME_LAYOUT_CONTAINERS.map((container) => container.key) as unknown as readonly string[],
            `${basePath}.container`,
            errors
          );
        }

        const grid = value['grid'];

        if (!isPlainObject(grid)) {
          errors.push(`${basePath}.grid must be an object.`);
          continue;
        }

        validateUnknownKeys(grid, GRID_KEYS, `${basePath}.grid`, errors);
        validateInteger(grid['x'], `${basePath}.grid.x`, errors, 0, true);
        validateInteger(grid['y'], `${basePath}.grid.y`, errors, 0, true);
        validateInteger(grid['w'], `${basePath}.grid.w`, errors, 0, false);
        validateInteger(grid['h'], `${basePath}.grid.h`, errors, 0, false);
      }
    }
  }

  const elements = input['elements'];

  if (elements !== undefined) {
    if (!isPlainObject(elements)) {
      errors.push('theme.elements must be an object.');
    } else {
      const allowedElementKeys = THEME_REGISTRY.map((entry) => entry.key);

      validateUnknownKeys(elements, allowedElementKeys, 'theme.elements', errors);

      for (const [key, value] of Object.entries(elements)) {
        validateElementStyles(value, `theme.elements.${key}`, errors);
      }
    }
  }

  const animations = input['animations'];

  if (animations !== undefined) {
    if (!isPlainObject(animations)) {
      errors.push('theme.animations must be an object.');
    } else {
      for (const [key, value] of Object.entries(animations)) {
        if (!SAFE_CLASS_PATTERN.test(key)) {
          errors.push(`theme.animations.${key} must use a safe CSS class token.`);
          continue;
        }

        validateAnimationDefinition(value, `theme.animations.${key}`, errors);
      }
    }
  }

  if (errors.length > 0) {
    return {
      valid: false,
      errors,
      value: null
    };
  }

  return {
    valid: true,
    errors: [],
    value: normaliseThemeDocument(input)
  };
}
