const createLess = require('less/lib/less').default;
const AbstractFileManager = require('less/lib/less/environment/abstract-file-manager').default;
const AbstractPluginLoader = require('less/lib/less/environment/abstract-plugin-loader').default;
const contexts = require('less/lib/less/contexts').default;

const CSS_VAR_PREFIX = 'og-smi';
const SMI_BLOCK_SELECTOR = 'og-smi';

// Variable Extractor Plugin for CSS Custom Properties
class VariableExtractorPlugin {
  constructor() {
    this.variables = {};
    this.context = null;
  }

  install(less, pluginManager) {
    pluginManager.addVisitor(this);
    pluginManager.addPostProcessor(this);
  }

  // Run after evaluation but before optimization
  get isPreVisitor() {
    return true;
  }

  // Called after Less has fully parsed and evaluated the AST — all @imports are already
  // inlined. For example, after processing main.less the tree looks like:
  //
  //   rootRuleset
  //   ├── @color-primary: #39355b
  //   ├── @color-badge-text: @color-primary
  //   └── og-smi  (Ruleset)
  //       └── @color-primary: #ff0000    (override from variables.less)
  run(rootRuleset, context) {
    try {
      // Get the ruleset from the og-smi block, which is where variable overrides are declared
      // (note: may not be present if the merchant has made customizations, but that is very unlikely)
      const smiBlockRuleset = this.getSmiBlockRuleset(rootRuleset);

      // Build a lookup context for resolving variable references inside values.
      // When a value like "@color-primary" appears inside another variable's value,
      // Less needs to know which definitions are in scope. We put the og-smi override
      // block first so merchant overrides win over the root-level defaults.
      const evalContext = new contexts.Eval(
        context || {},
        smiBlockRuleset ? [smiBlockRuleset, rootRuleset] : [rootRuleset]
      );
      this.context = evalContext;

      // First extract variables from the root level, then from the og-smi block if it exists.
      // This ensures that variables declared in the og-smi block (e.g. from variables.less) take precedence over root-level declarations.
      // Note: only variables declared directly inside the og-smi block are extracted as overrides. Variables declared in nested blocks (e.g. inside a .nested selector) are ignored since they are scoped and should not affect the global override values.
      this.extractVariablesFrom(rootRuleset);
      if (smiBlockRuleset) {
        this.extractVariablesFrom(smiBlockRuleset);
      }
    } catch (error) {
      console.error('Error extracting variables:', error);
    }

    return rootRuleset;
  }

  extractVariablesFrom(ruleset) {
    const vars = ruleset.variables();
    for (const name in vars) {
      if (Object.prototype.hasOwnProperty.call(vars, name)) {
        const declaration = ruleset.variable(name);
        const valueNode = declaration && declaration.value ? declaration.value : declaration;
        this.variables[name] = this.serializeValue(valueNode);
      }
    }
  }

  // Finds the og-smi { } block in the parsed stylesheet, which is where merchant
  // variable overrides live.
  getSmiBlockRuleset(ruleset) {
    return ruleset.rules?.find(node =>
      node.selectors?.some(selector => selector.elements?.[0]?.value === SMI_BLOCK_SELECTOR)
    );
  }

  serializeValue(valueNode) {
    let cssString = '';
    const output = {
      add: chunk => {
        cssString += chunk;
      }
    };

    try {
      if (valueNode && valueNode.genCSS) {
        // Check if the node needs evaluation (has an eval method and hasn't been evaled)
        let evaledNode = valueNode;
        if (valueNode.eval && typeof valueNode.eval === 'function') {
          // Try to evaluate the node to resolve variable references and functions
          try {
            evaledNode = valueNode.eval(this.context);
          } catch (e) {
            // If evaluation fails, use the original node
            evaledNode = valueNode;
          }
        }
        evaledNode.genCSS({}, output);
      }
    } catch (error) {
      console.error('Error serializing value:', error);
      return '';
    }

    return cssString;
  }

  // Post-processor to generate and prepend root og-smi block
  process(css, _extra) {
    const entries = Object.entries(this.variables);

    if (entries.length === 0) {
      return css;
    }

    const properties = entries
      .map(([name, value]) => {
        // Remove @ from variable name and add prefix
        // @color-primary -> --og-smi-color-primary
        const varName = name.startsWith('@') ? name.slice(1) : name;
        const cssVarName = `--${CSS_VAR_PREFIX}-${varName}`;
        return `  ${cssVarName}: ${value};`;
      })
      .join('\n');

    const rootBlock = `${SMI_BLOCK_SELECTOR} {\n${properties}\n}\n\n`;
    return rootBlock + css;
  }
}

const isNameEqual = name => it => it.name === name || it.name === `/${name}`;
function getMain(files) {
  const fallback = { content: '', name: '' };
  if (Array.isArray(files))
    return (
      files.find(isNameEqual('styles/main.less')) ||
      files.find(isNameEqual('styles/index.less')) ||
      files.find(isNameEqual('styles/styles.css')) ||
      fallback
    );
  return fallback;
}

function getRootPath(main) {
  return main.name.replace(/[^/]+$/, '');
}

async function smiLess(files) {
  class PluginLoader extends AbstractPluginLoader {
    loadPlugin(filename, basePath, context, environment, fileManager) {
      return new Promise((fulfill, reject) => {
        fileManager.loadFile(filename, basePath, context, environment).then(fulfill).catch(reject);
      });
    }
  }
  class FileManager extends AbstractFileManager {
    join(basePath, laterPath) {
      if (!basePath) {
        return laterPath;
      }
      return this.extractUrlParts(laterPath, basePath).path;
    }

    supports() {
      return true;
    }

    async loadFile(inFilename, currentDirectory, options, _environment) {
      let filename = inFilename;
      if (currentDirectory && !this.isPathAbsolute(filename)) {
        filename = currentDirectory + filename;
      }
      filename = options.ext ? this.tryAppendExtension(filename, options.ext) : filename;

      const { fileUrl } = this.extractUrlParts(filename, '');
      const file = files.find(it => it.name === fileUrl);
      if (file) {
        return {
          contents: file.content,
          filename: fileUrl
        };
      }
      if (inFilename.includes('://')) {
        return {
          contents: `@import (css) url(${inFilename});`,
          filename: fileUrl
        };
      }
      throw new Error(`File not found ${fileUrl}`);
    }
  }
  const myLess = createLess({}, [new FileManager()]);

  myLess.PluginLoader = PluginLoader;
  myLess.FileManager = FileManager;

  const variableExtractor = new VariableExtractorPlugin();

  const main = getMain(files);
  const rs = await myLess.render(main.content, {
    rootFileInfo: {
      currentDirectory: getRootPath(main),
      filename: main.name,
      rootFilename: main.name
    },
    rootpath: getRootPath(main),
    plugins: [variableExtractor]
  });

  return rs;
}

module.exports = smiLess;
module.exports.VariableExtractorPlugin = VariableExtractorPlugin;
