/**
 * packages/extract/src/token-source/scss-resolver.ts - best-effort Sass
 * compilation for SCSS token files.
 *
 * The text parsers in parse.ts intentionally stay lightweight, but SCSS token
 * sources often define public tokens through module variables, map lookups,
 * functions, and interpolation. This resolver compiles those sources against
 * the in-memory source file store and returns concrete CSS values for the
 * parsed token names.
 */

import { createRequire } from "node:module";
import path from "node:path/posix";
import type { Importer } from "sass";
import { deduplicateBySelector, parseCSSTokens } from "./parse.js";
import type { ParsedToken } from "./types.js";

const RESOLVED_VAR_PREFIX = "--__fragments-scss-token-";

function resolutionKey(file: string, name: string) {
  return `${file}\0${name}`;
}

export function getScssResolutionKey(file: string, name: string) {
  return resolutionKey(file, name);
}

function normalizeRepoPath(value: string) {
  return path.normalize(value.replace(/\\/g, "/").replace(/^\/+/, ""));
}

function repoUrl(filePath: string) {
  return new URL(`repo:///${normalizeRepoPath(filePath)}`);
}

function pathFromRepoUrl(url: URL) {
  return decodeURIComponent(url.pathname).replace(/^\/+/, "");
}

function importCandidates(rawSpecifier: string, fromFile?: string) {
  const specifier = rawSpecifier.split(/[?#]/)[0].replace(/\\/g, "/");
  const base = normalizeRepoPath(
    fromFile && !specifier.startsWith("/")
      ? path.join(path.dirname(fromFile), specifier)
      : specifier
  );
  const ext = path.extname(base);
  const dir = path.dirname(base);
  const name = path.basename(base);

  if (ext) {
    return [
      base,
      path.join(dir, `_${name}`),
      path.join(base, "index.scss"),
      path.join(base, "_index.scss"),
    ];
  }

  return [
    `${base}.scss`,
    path.join(dir, `_${name}.scss`),
    `${base}.sass`,
    path.join(dir, `_${name}.sass`),
    `${base}.css`,
    path.join(base, "index.scss"),
    path.join(base, "_index.scss"),
  ];
}

function createImporter(fileTexts: Map<string, string>): Importer<"async"> {
  const hasFile = (candidate: string) => fileTexts.has(normalizeRepoPath(candidate));
  const resolveCandidate = (specifier: string, fromFile?: string) => {
    for (const candidate of importCandidates(specifier, fromFile)) {
      const normalized = normalizeRepoPath(candidate);
      if (hasFile(normalized)) return normalized;
    }
    return null;
  };

  return {
    canonicalize(url, options) {
      if (url.startsWith("sass:") || url.startsWith("pkg:")) return null;

      if (url.startsWith("repo:")) {
        const parsed = new URL(url);
        const repoPath = pathFromRepoUrl(parsed);
        const exact = normalizeRepoPath(repoPath);
        if (hasFile(exact)) return repoUrl(exact);
        const resolved = resolveCandidate(repoPath);
        return resolved ? repoUrl(resolved) : null;
      }

      const containingUrl = options.containingUrl;
      const containingFile =
        containingUrl?.protocol === "repo:" ? pathFromRepoUrl(containingUrl) : undefined;
      const resolved = resolveCandidate(url, containingFile);
      return resolved ? repoUrl(resolved) : null;
    },
    load(canonicalUrl) {
      if (canonicalUrl.protocol !== "repo:") return null;
      const filePath = normalizeRepoPath(pathFromRepoUrl(canonicalUrl));
      const contents = fileTexts.get(filePath);
      if (contents === undefined) return null;
      const lower = filePath.toLowerCase();
      const syntax = lower.endsWith(".sass") ? "indented" : lower.endsWith(".css") ? "css" : "scss";
      return { contents, syntax };
    },
  };
}

function cssVarForScssToken(tokenName: string) {
  return `${RESOLVED_VAR_PREFIX}${tokenName.replace(/^\$/, "")}`;
}

function stripResolvedVarName(name: string) {
  return `$${name.slice(RESOLVED_VAR_PREFIX.length)}`;
}

function publicCssVariableMixins(content: string) {
  const mixins: string[] = [];
  const regex = /@mixin\s+([a-zA-Z0-9_-]*css-variables[a-zA-Z0-9_-]*)\b/g;
  let match: RegExpExecArray | null;
  while ((match = regex.exec(content)) !== null) {
    const name = match[1];
    if (!name.startsWith("_") && !name.startsWith("-")) mixins.push(name);
  }
  return [...new Set(mixins)];
}

function roundRgbValues(value: string): string {
  return value
    .replace(/rgb\(([^)]+)\)/g, (_full, inner: string) => {
      const rounded = inner
        .split(",")
        .map((part) => {
          const num = parseFloat(part.trim());
          return Number.isNaN(num) ? part.trim() : String(Math.round(num));
        })
        .join(", ");
      return `rgb(${rounded})`;
    })
    .replace(/rgba\(([^)]+)\)/g, (_full, inner: string) => {
      const rounded = inner
        .split(",")
        .map((part, index) => {
          if (index >= 3) return part.trim();
          const num = parseFloat(part.trim());
          return Number.isNaN(num) ? part.trim() : String(Math.round(num));
        })
        .join(", ");
      return `rgba(${rounded})`;
    });
}

function shouldCompileScssVariable(token: ParsedToken) {
  if (!token.name.startsWith("$")) return false;
  const raw = token.value.trim();
  if (!raw || raw.startsWith("(")) return false;
  return true;
}

// Sass map/function resolution is a best-effort enhancement on top of the
// regex-based parser in `parse.ts`. We deliberately load `sass` via
// `createRequire(...)` rather than `import('sass')`: Vite/Rolldown
// inline sass into a server chunk even with `ssr.external: ['sass']`,
// and the inlined chunk drops `sass.dart.js` (the Dart payload that
// registers `globalThis._cliPkgExports`) so the wrapper's
// `_cliPkgExports.pop()` crashes at module init on Vercel.
// `createRequire` is opaque to the bundler, so Node's real loader runs
// both files in the right order and the global handoff works. The
// try/catch keeps the scan resilient if sass is ever missing at runtime
// (e.g., a build that strips devDeps before our dep-bump propagates).
type SassModule = typeof import("sass");
let cachedSassModule: SassModule | null | undefined;
function loadSassModule(): SassModule | null {
  if (cachedSassModule !== undefined) return cachedSassModule;
  try {
    const require = createRequire(import.meta.url);
    cachedSassModule = require("sass") as SassModule;
  } catch {
    cachedSassModule = null;
  }
  return cachedSassModule;
}

async function compileScss(source: string, importer: Importer<"async">): Promise<string | null> {
  const sass = loadSassModule();
  if (!sass) return null;
  try {
    const result = await sass.compileStringAsync(source, {
      importers: [importer],
      style: "expanded",
      logger: sass.Logger.silent,
    });
    return result.css;
  } catch {
    return null;
  }
}

async function resolveScssVariables(args: {
  filePath: string;
  importer: Importer<"async">;
  tokens: ParsedToken[];
}) {
  const scssTokens = args.tokens.filter(shouldCompileScssVariable);
  const resolved = new Map<string, string>();
  if (scssTokens.length === 0) return resolved;

  const declarations = scssTokens
    .map((token) => `  ${cssVarForScssToken(token.name)}: #{mod.${token.name}};`)
    .join("\n");
  const source = `@use "${repoUrl(args.filePath).href}" as mod;\n:root {\n${declarations}\n}`;
  const css = await compileScss(source, args.importer);
  if (!css) return resolved;

  const regex = new RegExp(
    `(${RESOLVED_VAR_PREFIX.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}[\\w-]+)\\s*:\\s*([^;]+);`,
    "g"
  );
  let match: RegExpExecArray | null;
  while ((match = regex.exec(css)) !== null) {
    const tokenName = stripResolvedVarName(match[1]);
    resolved.set(resolutionKey(args.filePath, tokenName), roundRgbValues(match[2].trim()));
  }

  return resolved;
}

async function resolveCssCustomProperties(args: {
  filePath: string;
  importer: Importer<"async">;
  content: string;
  tokens: ParsedToken[];
}) {
  const cssTokenNames = new Set(
    args.tokens.filter((token) => token.name.startsWith("--")).map((token) => token.name)
  );
  const resolved = new Map<string, string>();
  if (cssTokenNames.size === 0) return resolved;

  const includes = publicCssVariableMixins(args.content)
    .map((mixin) => `@include mod.${mixin};`)
    .join("\n");
  const source = `@use "${repoUrl(args.filePath).href}" as mod;\n${includes}`;
  const css = await compileScss(source, args.importer);
  if (!css) return resolved;

  const parsed = deduplicateBySelector(parseCSSTokens(css, args.filePath));
  for (const token of parsed) {
    if (!cssTokenNames.has(token.name)) continue;
    resolved.set(resolutionKey(args.filePath, token.name), roundRgbValues(token.value));
  }
  return resolved;
}

export async function resolveScssTokenValues(args: {
  tokens: ParsedToken[];
  fileTexts: Map<string, string>;
}): Promise<Map<string, string>> {
  const scssFiles = [...args.fileTexts.keys()].filter((filePath) =>
    filePath.toLowerCase().endsWith(".scss")
  );
  if (scssFiles.length === 0) return new Map();

  const importer = createImporter(args.fileTexts);
  const tokensByFile = new Map<string, ParsedToken[]>();
  for (const token of args.tokens) {
    if (!token.file.toLowerCase().endsWith(".scss")) continue;
    const existing = tokensByFile.get(token.file) ?? [];
    existing.push(token);
    tokensByFile.set(token.file, existing);
  }

  const resolved = new Map<string, string>();
  for (const filePath of scssFiles) {
    const fileTokens = tokensByFile.get(filePath);
    const content = args.fileTexts.get(filePath);
    if (!fileTokens?.length || content === undefined) continue;

    const [variables, cssProperties] = await Promise.all([
      resolveScssVariables({ filePath, importer, tokens: fileTokens }),
      resolveCssCustomProperties({
        filePath,
        importer,
        content,
        tokens: fileTokens,
      }),
    ]);

    for (const [key, value] of variables) resolved.set(key, value);
    for (const [key, value] of cssProperties) resolved.set(key, value);
  }

  return resolved;
}
