/* eslint-disable @typescript-eslint/no-explicit-any */
import { getExternalContentMock } from './mock';
import { loadModule, renderServerModule } from './utils';

let distFolderPath: string | null = null;
let currentApp: any = null;

export function setDistFolderPath(path: string): void {
  distFolderPath = path;
}

// ============================================================================
// Module Loading Helpers
// ============================================================================

/**
 * Loads all required modules for a showcase.
 */
async function loadShowcaseModules(sectionName: string, showcaseId: string, distFolder: string): Promise<[any, any, any, any, any]> {
  const basePath = `${distFolder}/sections/${sectionName}`;
  return Promise.all([
    loadModule(`${basePath}/js/settings/content.mjs`),
    loadModule(`${basePath}/js/settings/translations.mjs`),
    loadModule(`${basePath}/js/showcases/translations.mjs`),
    loadModule(`${basePath}/js/showcases/${showcaseId}.mjs`),
    loadModule(`${basePath}/js/settings/design.mjs`),
  ]) as Promise<[any, any, any, any, any]>;
}

interface HSLColor {
  h: number;
  s: number;
  l: number;
  a: number;
}

interface RGBAColor {
  r: number;
  g: number;
  b: number;
  a: number;
}

interface ColorObject {
  hex: string;
  hsl: HSLColor;
  rgba: RGBAColor;
  auto: boolean;
  raw: string;
}

function hexToColorObject(hex: string): ColorObject {
  // Support both 6-digit (#RRGGBB) and 8-digit (#RRGGBBAA) hex colors
  const match = /^#?([0-9a-fA-F]{6}([0-9a-fA-F]{2})?)$/.exec(hex);
  if (!match) throw new Error('Invalid hex color format');
  const cleanHex = match[1].toLowerCase();
  const r = parseInt(cleanHex.substring(0, 2), 16);
  const g = parseInt(cleanHex.substring(2, 4), 16);
  const b = parseInt(cleanHex.substring(4, 6), 16);
  // Extract alpha if present (8-digit hex), otherwise default to 255 (fully opaque)
  const a = cleanHex.length === 8 ? parseInt(cleanHex.substring(6, 8), 16) : 255;
  const rNorm = r / 255, gNorm = g / 255, bNorm = b / 255;
  const max = Math.max(rNorm, gNorm, bNorm), min = Math.min(rNorm, gNorm, bNorm);
  const delta = max - min;
  let h = 0, s = 0;
  const l = (max + min) / 2;
  if (delta !== 0) {
    s = delta / (1 - Math.abs(2 * l - 1));
    switch (max) {
      case rNorm: {
        h = ((gNorm - bNorm) / delta) % 6;
        break;
      }
      case gNorm: {
        h = (bNorm - rNorm) / delta + 2;
        break;
      }
      case bNorm: {
        h = (rNorm - gNorm) / delta + 4;
        break;
      }
    }
    h *= 60;
    if (h < 0) h += 360;
  }
  return {
    hex: `#${cleanHex.substring(0, 6)}${a.toString(16).padStart(2, '0')}`,
    hsl: {
      h: +(h / 360).toFixed(4),
      s: +s.toFixed(4),
      l: +l.toFixed(4),
      a: +(a / 255).toFixed(2),
    },
    rgba: { r, g, b, a: +(a / 255).toFixed(2) },
    auto: false,
    raw: `#${cleanHex.substring(0, 6).toUpperCase()}`,
  };
}

function updateHexColors(obj: any): any {
  // Matches 3-digit (#RGB), 6-digit (#RRGGBB), or 8-digit (#RRGGBBAA) hex
  const hexRegex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;

  function recurse(value: any): any {
    if (Array.isArray(value)) return value.map(recurse);
    if (value && typeof value === 'object') {
      for (const key in value) {
        const v = value[key];
        if (key === 'color' && typeof v === 'string' && hexRegex.test(v)) {
          // If it’s a 3-digit hex, expand it to 6 digits
          const expanded = v.replace(
            /^#?([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/,
            (_m, r, g, b) => `#${r}${r}${g}${g}${b}${b}`,
          );
          value[key] = hexToColorObject(expanded);
        } else {
          value[key] = recurse(v);
        }
      }
    }
    return value;
  }
  return recurse(obj);
}

const replaceGlobalFont = (obj: any, font: string): void => {
  Object.entries(obj).forEach(([k, v]) => {
    if (v && typeof v === 'object') {
      replaceGlobalFont(v, font);
    } else if (k === 'font' && v === 'global.fontFamily.body') {
      obj[k] = font;
    }
  });
};

function createBackgroundObject(type: string, fromColor: any, toColor: any): any {
  return {
    background: {
      type: type,
      solid: {
        color: fromColor,
      },
      gradient: { fromColor: fromColor, toColor: toColor },
    },
  };
}

function createBackgroundDesign(showcaseBackground: any, isApiRender: boolean = false): any {
  // Helper to check if color is global reference
  const isGlobalColor = (color: string) => typeof color === 'string' && color.startsWith('global.');
  const defaultGrayColor = hexToColorObject('#F9F9F9');

  // No background or invalid background
  if (!showcaseBackground) {
    const defaultColor = 'global.color.background';
    if (isApiRender) {
      // API render: keep global as string
      return createBackgroundObject('solid', defaultColor, defaultColor);
    } else {
      // Local preview: convert to color object
      return createBackgroundObject('solid', defaultGrayColor, defaultGrayColor);
    }
  }

  // Handle gradient backgrounds
  if (showcaseBackground.style === 'GRADIENT' && Array.isArray(showcaseBackground.color)) {
    const [fromColor, toColor] = showcaseBackground.color;

    // Check if colors are global references
    const fromIsGlobal = isGlobalColor(fromColor);
    const toIsGlobal = isGlobalColor(toColor);
    if (isApiRender) {
      // Convert each color: keep global as string, convert hex to object
      const fromColorValue = fromIsGlobal ? fromColor : hexToColorObject(fromColor);
      const toColorValue = toIsGlobal ? toColor : hexToColorObject(toColor);
      return createBackgroundObject('gradient', fromColorValue, toColorValue);
    } else {
      const fromColorValue = fromIsGlobal ? defaultGrayColor : hexToColorObject(fromColor);
      const toColorValue = toIsGlobal ? defaultGrayColor : hexToColorObject(toColor);
      return createBackgroundObject('gradient', fromColorValue, toColorValue);
    }
  }

  // Handle solid color backgrounds
  if (showcaseBackground.style === 'COLOR') {
    // According to source code array is also possible
    const showcaseColor = showcaseBackground.color;
    const solidColor = Array.isArray(showcaseColor) ? showcaseColor[0] : showcaseColor;
    if (isApiRender) {
      const finalColor = isGlobalColor(solidColor) ? solidColor : hexToColorObject(solidColor);
      return createBackgroundObject('solid', finalColor, finalColor);
    } else {
      // Hex color - convert to color object
      const finalColor = isGlobalColor(solidColor) ? defaultGrayColor : hexToColorObject(solidColor);
      return createBackgroundObject('solid', finalColor, finalColor);
    }
  }
}

function overrideSettingsFromShowcase(content: any, showcases: any): any {
  return Object.fromEntries(Object.entries(content).map(([k, v]) => [k, showcases[k] ?? v]));
}

export function mergeDesign(
  design: Record<string, any>,
  showcase: Record<string, any>,
): Record<string, any> {
  const result: Record<string, any> = { ...design };

  Object.keys(design).forEach((key) => {
    if (key in showcase) {
      const base = design[key];
      const override = showcase[key];

      if (
        base !== null && override !== null
        && typeof base === 'object' && typeof override === 'object'
        && !Array.isArray(base) && !Array.isArray(override)
      ) {
        // shallow merge nested objects
        result[key] = { ...base, ...override };
      } else {
        // replace with showcase value (covers primitives & arrays)
        result[key] = override;
      }
    }
  });

  return result;
}

export function designTransformer(design: Record<string, any>, showCaseDesign: Record<string, any>): Record<string, any> {
  const parsedDesign: Record<string, any> = {};
  const parsedShowcaseDesign: Record<string, any> = {};
  Object.entries(design).forEach(([key, comp]) => {
    // Skip DIVIDER elements - they're UI-only and don't have runtime data
    if (comp?.type === 'DIVIDER') {
      return;
    }
    // Skip BACKGROUND - it's handled separately by createBackgroundDesign
    if (comp?.type === 'BACKGROUND') {
      return;
    }
    // Use defaults if available, otherwise use empty object
    parsedDesign[key] = comp?.defaults || {};
  });

  Object.entries(showCaseDesign).forEach(([key, comp]) => {
    // Skip DIVIDER elements - they're UI-only and don't have runtime data
    if (comp?.type === 'DIVIDER') {
      return;
    }
    // Skip BACKGROUND - it's handled separately by createBackgroundDesign
    if (comp?.type === 'BACKGROUND') {
      return;
    }
    const { type, ...withoutType } = comp;
    parsedShowcaseDesign[key] = type == 'TEXT' ? { ...withoutType, visible: true } : withoutType;
  });

  let overridenDesign = mergeDesign(parsedDesign, parsedShowcaseDesign);
  overridenDesign = updateHexColors(overridenDesign);
  replaceGlobalFont(overridenDesign, 'Roboto');
  return overridenDesign;
}

function processImage(component: any, sectionName: string, key: string): Record<string, any> {
  const assetLocation = `${distFolderPath}/sections/${sectionName}/assets/`;
  const set = component.defaults?.set || component.imageData?.set;
  const newSet = {
    'cropped-webp-100x200': { url: assetLocation + set.MOBILE_WEBP_LOW_RES.url },
    'cropped-webp-1000x2000': { url: assetLocation + set.MOBILE_WEBP_HI_RES.url },
    'webp-200x200': { url: assetLocation + set.WEBP_LOW_RES.url },
    'webp-2000x2000': { url: assetLocation + set.WEBP_HI_2X_RES.url },
  };
  return {
    [key]: {
      set: newSet,
      ...(component.imageData?.borderInfo && { borderInfo: component.imageData.borderInfo }),
      ...(component.imageData && { bucket: {} }),
    },
  };
}

function processComponent(key: string, component: any, translations: any, sectionName?: string): any {
  if (!component?.type) return {};
  switch (component.type) {
    case 'INPUTBOX':
    case 'TEXTAREA': {
      const text = component?.defaults?.text ?? component?.text ?? component?.placeholder;
      return { [key]: translations[text] };
    }
    case 'DIVIDER':
      return { [key]: translations[component.label] };
    case 'DECK': {
      const defaultSettings = component.cards?.defaultCardContent?.settings;
      if (defaultSettings) {
        const result: Record<string, any> = {};
        Object.entries(defaultSettings).forEach(([k, c]) => {
          Object.assign(result, processComponent(k, c, translations));
        });
        return { [key]: result };
      } else {
        const cards = component.cards.map((card: any) => {
          const cardContent: Record<string, any> = {};
          Object.entries(card.settings).forEach(([k, c]) => {
            Object.assign(cardContent, processComponent(k, c, translations, sectionName));
          });
          return { settings: cardContent };
        });
        return { [key]: { cards } };
      }
    }
    case 'BUTTON': {
      const button = component.defaults || component;
      return {
        [key]: {
          title: translations[button.title],
          type: button.buttonType,
          link: button.link,
        },
      };
    }
    case 'TOGGLE': {
      return {
        [key]: {
          enabled: component.defaults?.enabled ?? component.enabled
        }
      };
    }
    case 'SELECTBOX':
      return { [key]: component.defaults?.value ?? component.value };
    case 'IMAGE':
      return processImage(component, sectionName!, key);
    case 'INFO':
      return {
        [key]: {
          label: translations[component.label],
          description: translations[component.description],
          button: {
            label: translations[component.button.label],
            link: component.button.link,
          },
        },
      };
    default:
      return {};
  }
}

function getContentToRender(
  content: any,
  showcase: any,
  contentTranslations: any,
  showcaseTranslations: any,
  sectionName: string,
): any {
  const parsedContent = Object.entries(content).reduce((acc, [k, c]) => {
    return { ...acc, ...processComponent(k, c, contentTranslations, sectionName) };
  }, {});

  const parsedShowcase = Object.entries(showcase).reduce((acc, [k, c]) => {
    return { ...acc, ...processComponent(k, c, showcaseTranslations, sectionName) };
  }, {});

  return overrideSettingsFromShowcase(parsedContent, parsedShowcase);
}

export function dropdownOptions(showcaseModules: Record<string, any>): Array<{ value: string; label: string }> {
  return Object.keys(showcaseModules).map((path) => {
    const match = path.match(/\/sections\/([^/]+)\/js\/showcases\/(\d+)\.mjs$/);
    if (!match) return null;
    return {
      value: `${match[1]}:${match[2]}`,
      label: `${match[1]}: showcase ${match[2]}`,
    };
  }).filter(Boolean) as Array<{ value: string; label: string }>;
}

export function loadSectionCss(sectionName: string): void {
  const link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = `${distFolderPath}/sections/${sectionName}/js/main/client/assets/client.css`;
  document.head.appendChild(link);
}

/**
 * Renders a showcase in the browser (client-side).
 */
export async function renderShowcase(sectionName: string, showcaseId: string): Promise<void> {
  if (!distFolderPath) throw new Error('distFolderPath not set');

  const basePath = `${distFolderPath}/sections/${sectionName}`;
  const [content, contentTranslations, showcaseTranslations, showcase, design]
    = await loadShowcaseModules(sectionName, showcaseId, distFolderPath);

  const client = await loadModule(`${basePath}/js/main/client/client.js`) as any;

  // Prepare design
  const showcaseBackground = (showcase as any).default?.design?.background;
  const backgroundDesign = createBackgroundDesign(showcaseBackground, false); // Local preview
  const overriddenDesign = designTransformer((design as any).default, (showcase as any).default.design || {});
  overriddenDesign.background = backgroundDesign;

  // Prepare content
  const overriddenContent = getContentToRender(
    (content as any).default,
    (showcase as any).default.content || {},
    (contentTranslations as any).default.en,
    (showcaseTranslations as any).default.en,
    sectionName,
  );

  // Load CSS
  loadSectionCss(sectionName);

  // Prepare state
  const state = {
    context: { globalDesign: { color: 'global.color.background' } },
    data: {
      content: overriddenContent,
      design: overriddenDesign,
      defaults: {},
      background: {},
      externalContent: getExternalContentMock(),
    },
  };

  // Mount or update app
  if (currentApp) {
    currentApp.update(state);
  } else {
    const { mount, update, unmount } = (client as any).default.init();
    currentApp = { update, unmount };
    mount('#app', state);
  }
}

async function getLayout(showcase: any, sectionName: string, distFolder: string) {
  // First, try to get layoutId from showcase
  if (showcase.default?.layoutId) {
    return showcase.default.layoutId;
  }

  // If not present in showcase, read from section's layout.mjs
  const basePath = `${distFolder}/sections/${sectionName}`;
  const layoutModule = await loadModule(`${basePath}/js/settings/layout.mjs`) as any;

  // layout.mjs exports an array of layout configurations
  // Return the first element's layoutId
  if (Array.isArray(layoutModule.default) && layoutModule.default.length > 0) {
    return layoutModule.default[0].layoutId;
  }
  throw new Error('Layout module is not an array or is empty');
}

/**
 * Prepares showcase data (content and design) without rendering.
 * Used by API routes to return tile data.
 */
export async function getShowcaseData(sectionName: string, showcaseId: string, distFolder: string) {
  const [content, contentTranslations, showcaseTranslations, showcase, design]
    = await loadShowcaseModules(sectionName, showcaseId, distFolder);

  // Prepare design
  const showcaseBackground = (showcase as any).default?.design?.background;
  const backgroundDesign = createBackgroundDesign(showcaseBackground, true); // API render
  const overriddenDesign = designTransformer((design as any).default, (showcase as any).default.design || {});
  overriddenDesign.background = backgroundDesign;
  overriddenDesign.layout = await getLayout(showcase, sectionName, distFolder);

  // Prepare content
  const overriddenContent = getContentToRender(
    (content as any).default,
    (showcase as any).default.content || {},
    (contentTranslations as any).default.en,
    (showcaseTranslations as any).default.en,
    sectionName,
  );

  return {
    content: overriddenContent,
    design: overriddenDesign,
  };
}

/**
 * Gets the showcase state by rendering server-side.
 * Uses renderServerModule to get HTML directly from the SSR server.
 */
export async function getShowcaseState(sectionName: string, showcaseId: string, distFolder: string, context: Record<string, any>) {
  const { content: overriddenContent, design: overriddenDesign }
    = await getShowcaseData(sectionName, showcaseId, distFolder);

  const data = {
    content: overriddenContent,
    design: overriddenDesign,
    defaults: {},
    background: {},
    externalContent: getExternalContentMock(),
  };

  // Render server-side using the SSR server
  const basePath = `${distFolder}/sections/${sectionName}`;
  const serverModulePath = `${basePath}/js/main/server/server.js`;
  const html = await renderServerModule(serverModulePath, context, data);

  return {
    content: overriddenContent,
    design: overriddenDesign,
    html,
  };
}
