import merge from 'lodash.merge';
import { BASE_THEME_SECTION, DESCOPE_PREFIX } from '../../constants';
import { isUrl, kebabCase } from '..';
import { getComponentName, getCssVarName } from '../componentHelpers';

const getVarName = (path) => getCssVarName(DESCOPE_PREFIX, ...path);

// lodash.set alternative
const set = (obj, path, value) => {
  const pathArray = Array.isArray(path) ? path : path.match(/([^[.\]])+/g);

  pathArray.reduce((acc, key, i) => {
    if (acc[key] === undefined) acc[key] = {};
    if (i === pathArray.length - 1) acc[key] = value;
    return acc[key];
  }, obj);

  return obj;
};

const transformTheme = (theme, path, getTransformation) =>
  Object.entries(theme).reduce((acc, [key, val]) => {
    if (val?.constructor !== Object) {
      return merge(acc, getTransformation(path.concat(key), val));
    }
    return merge(acc, transformTheme(val, [...path, key], getTransformation));
  }, {});

const stringifyArray = (strArr) =>
  strArr.map((str) => (str.includes(' ') ? `"${str}"` : str)).join(', ');

const getCssVarValue = (val) => {
  switch (true) {
    case Array.isArray(val):
      return stringifyArray(val);
    case isUrl(val):
      return `url(${val})`;
    default:
      return val;
  }
};

export const themeToCSSVarsObj = (theme) =>
  transformTheme(theme, [], (path, val) => ({
    [getVarName(path)]: getCssVarValue(val),
  }));

export const getThemeRefs = (theme, prefix) =>
  transformTheme(theme, [], (path) =>
    set({}, path, `var(${getVarName(prefix ? [prefix, ...path] : path)})`)
  );

export const getThemeVars = (theme, prefix) =>
  transformTheme(theme, [], (path) => set({}, path, getVarName(prefix ? [prefix, ...path] : path)));

export const globalsThemeToStyle = (theme, themeName = '') => {
  const style = Object.entries(themeToCSSVarsObj(theme)).reduce(
    (acc, entry) => `${acc}${entry.join(':')};\n`,
    ''
  );

  if (!themeName) return style;

  return `*[data-theme="${themeName}"] {${style}}`;
};

function splitAmpersands(str) {
  const match = str.match(/^(&+)?(.*)$/);
  return [match[1] || '', match[2] || ''];
}

const CONTAINER_QUERY_PREFIX = '@container';
const isContainerQuery = (key) => key.startsWith(CONTAINER_QUERY_PREFIX);

const BREAKPOINTS_KEY = '$breakpoints';
const BREAKPOINTS_REF_PREFIX = '$breakpoints.';
const isBreakpointRef = (key) => key.startsWith(BREAKPOINTS_REF_PREFIX);

// st attributes are also using selector multiplication
// so we need to limit the multiplication
const MAX_SELECTOR_MULTIPLY = 3;

const componentsThemeToStyleObj = (componentsTheme) =>
  transformTheme(componentsTheme, [], (path, val) => {
    const [component, ...restPath] = path;

    // skip $breakpoints definitions — they are metadata, not CSS output
    if (restPath[0] === BREAKPOINTS_KEY) return {};

    const property = restPath.pop();
    const componentName = getComponentName(component);

    if (property === 'undefined') {
      // eslint-disable-next-line no-console
      console.warn(componentName, `theme value: "${val}" is mapped to an invalid property`);
    }

    const themeName = BASE_THEME_SECTION;

    // extract @container queries from restPath at any position,
    // resolving breakpoints.X references via the component's $breakpoints map
    const componentBreakpoints = componentsTheme[component]?.[BREAKPOINTS_KEY] || {};
    const resolveKey = (key) =>
      isBreakpointRef(key)
        ? `${CONTAINER_QUERY_PREFIX} ${componentBreakpoints[key.slice(BREAKPOINTS_REF_PREFIX.length)]}`
        : key;
    const breakpointRefs = restPath.filter(isBreakpointRef);
    const missingRef = breakpointRefs.find(
      (key) => !componentBreakpoints[key.slice(BREAKPOINTS_REF_PREFIX.length)]
    );
    if (missingRef) {
      // eslint-disable-next-line no-console
      console.warn(componentName, `theme references undefined breakpoint "${missingRef}"`);
      return {};
    }
    const containerQueries = breakpointRefs.map(resolveKey);
    // remove breakpoint refs from restPath so the remaining parts are processed as attribute selectors
    restPath.splice(0, restPath.length, ...restPath.filter((s) => !isBreakpointRef(s)));

    // do not start with underscore -> key:value, must have 2 no underscore attrs in a row
    // starts with underscore -> attribute selector
    const attrsSelector = restPath.reduce((acc, section, idx) => {
      const [ampersands, content] = splitAmpersands(section);
      const selectorMultiplier = Math.min(
        ampersands.length + 1, // if there are no & we need to multiply by 1
        MAX_SELECTOR_MULTIPLY
      );

      if (content.startsWith('_'))
        return acc + `[${kebabCase(content.replace(/^_/, ''))}="true"]`.repeat(selectorMultiplier);

      const nextSection = restPath[idx + 1];

      if (
        typeof nextSection !== 'string' ||
        nextSection.startsWith('_') ||
        nextSection.startsWith('&')
      ) {
        // eslint-disable-next-line no-console
        console.error(
          'theme generator',
          `your theme structure is invalid, attribute "${section}" is followed by "${nextSection}" which is not allowed`
        );
        return acc;
      }

      return (
        acc +
        `[${kebabCase(content)}="${restPath.splice(idx + 1, 1).join('')}"]`.repeat(
          selectorMultiplier
        )
      );
    }, '');

    const selector = `:host${attrsSelector ? `(${attrsSelector})` : ''}`;

    // wrap the selector rule in container queries from innermost to outermost
    // e.g. ['@container (max-width: 80px)'] + { ':host': { prop: val } }
    //   => { '@container (max-width: 80px)': { ':host': { prop: val } } }
    const leaf = containerQueries.reduceRight((acc, query) => ({ [query]: acc }), {
      [selector]: { [property]: getCssVarValue(val) },
    });

    return {
      [componentName]: {
        [themeName]: leaf,
      },
    };
  });

const componentsThemeToStyle = (componentsTheme) =>
  Object.entries(componentsTheme).reduce((acc, [key, value]) => {
    if (isContainerQuery(key)) {
      return `${acc}${key} {\n${componentsThemeToStyle(value)}}\n\n`;
    }
    return `${acc}${key} { \n${Object.entries(value)
      .map(([prop, val]) => `${prop}: ${val}`)
      .join(';\n')} \n}\n\n`;
  }, '');

export const createComponentsTheme = (componentsTheme) => {
  const styleObj = componentsThemeToStyleObj(componentsTheme);

  return Object.keys(styleObj).reduce((acc, componentName) => {
    const componentThemes = styleObj[componentName];

    return Object.assign(acc, {
      [componentName]: Object.keys(componentThemes).reduce(
        (res, theme) =>
          Object.assign(res, {
            [theme]: componentsThemeToStyle(componentThemes[theme]),
          }),
        {}
      ),
    });
  }, {});
};

export const themeToStyle = ({ globals, components }, themeName) => ({
  globals: globalsThemeToStyle(globals, themeName),
  components: createComponentsTheme(components),
});

// allows to generate css variables with nested fallbacks
export const useVar = (...varNames) =>
  varNames.reduceRight((acc, value) => {
    if (value.startsWith('--')) return `var(${value}${acc ? `, ${acc}` : acc})`;

    return `${value}${acc ? `, ${acc}` : acc}`;
  }, '');

export const createHelperVars = (theme, prefix) => {
  const res = transformTheme(theme, [], (path, value) => {
    const modifiedPath = [...path];
    const property = modifiedPath.splice(-1);
    const varName = getCssVarName(prefix, property);

    const vars = { [property]: varName };
    const useVars = { [property]: useVar(varName) };

    return { theme: set({}, [...modifiedPath, varName], value), useVars, vars };
  });

  return [res.theme, res.useVars, res.vars];
};

export { componentsThemeManager } from '@descope-ui/common/theme-helpers';
export * from './colorsHelpers';
export * from './resetHelpers';
