/**
 * CSS/SCSS declaration extractor — turns one source file into
 * `style_declaration` facts.
 *
 * Pure function over (source, filePath). Walks the source as a small character
 * machine that tracks selector depth, handles nested `&.primary`-style SCSS
 * selectors, ignores `// line` and `/* block * /` comments, and emits one fact
 * per CSS declaration. Identity (file + selector + declarationPath + property)
 * is stable when other lines move; locations point at the property name.
 *
 * SCSS variables, mixins, includes, and `@`-rule directives are intentionally
 * skipped — only real CSS declarations become facts. That keeps Phase 5 rules
 * simple and avoids leaking SCSS-only constructs (`$foo: red;`) into the fact
 * IR as if they were declarations.
 */

import {
  type Fact,
  makeThemeDeclarationFact,
  makeStyleDeclarationFact,
  type FactLocation,
} from "@fragments-sdk/core";

interface ParseContext {
  source: string;
  file: string;
  // Stripped copy where comments are blanked out (with newlines preserved) so
  // line/column from offsets stay correct.
  stripped: string;
  selectorStack: string[];
  themeStack: Array<{
    inline: boolean;
    flavor: "default" | "inline" | "reference" | "static" | "unknown";
  }>;
  /** Per-selector counters used for declarationPath stability. */
  declCounters: Map<string, number>;
  facts: Fact[];
  lineStarts: number[];
}

export function extractScssFacts(source: string, filePath: string): Fact[] {
  if (!source) return [];
  const stripped = stripComments(source);
  const ctx: ParseContext = {
    source,
    file: filePath,
    stripped,
    selectorStack: [],
    themeStack: [],
    declCounters: new Map(),
    facts: [],
    lineStarts: buildLineStarts(source),
  };
  parseBlock(ctx, 0, stripped.length);
  return ctx.facts;
}

// ---------------------------------------------------------------------------
// Comment stripping (preserves whitespace + newlines so offsets stay aligned)
// ---------------------------------------------------------------------------

const COMMENT_PATTERN = /(?:\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/)/g;

function stripComments(src: string): string {
  return src.replace(COMMENT_PATTERN, (match) => match.replace(/[^\n]/g, " "));
}

// ---------------------------------------------------------------------------
// Line/column resolution
// ---------------------------------------------------------------------------

function buildLineStarts(content: string): number[] {
  const starts = [0];
  for (let i = 0; i < content.length; i++) {
    if (content.charCodeAt(i) === 10 /* \n */) starts.push(i + 1);
  }
  return starts;
}

function lineColFromOffset(lineStarts: number[], offset: number): { line: number; column: number } {
  let lo = 0;
  let hi = lineStarts.length - 1;
  while (lo < hi) {
    const mid = (lo + hi + 1) >>> 1;
    if (lineStarts[mid] <= offset) lo = mid;
    else hi = mid - 1;
  }
  return { line: lo + 1, column: offset - lineStarts[lo] };
}

// ---------------------------------------------------------------------------
// Recursive-descent parser
// ---------------------------------------------------------------------------

/**
 * Walks a region of the stripped source from `[start, end)`, treating `{`/`}`
 * as block boundaries and `;` as declaration terminators. Recurses into nested
 * blocks while tracking the selector stack.
 */
function parseBlock(ctx: ParseContext, start: number, end: number): void {
  let i = start;
  let segmentStart = i;

  while (i < end) {
    const ch = ctx.stripped[i];

    if (ch === '"' || ch === "'") {
      i = skipString(ctx.stripped, i, ch);
      continue;
    }

    if (ch === "(") {
      i = skipBalanced(ctx.stripped, i, "(", ")");
      continue;
    }

    if (ch === "{") {
      const headerRaw = ctx.stripped.slice(segmentStart, i);
      const header = headerRaw.trim();
      // SCSS interpolation `#{...}` would have been opened earlier as `(`/`)`-
      // like; here we only see real block openers.
      const blockEnd = matchBrace(ctx.stripped, i);
      if (blockEnd === -1) return; // malformed — bail.
      const isAtRule = header.startsWith("@");
      if (isAtRule) {
        const theme = parseThemeHeader(header);
        if (theme) {
          ctx.themeStack.push(theme);
          parseBlock(ctx, i + 1, blockEnd);
          ctx.themeStack.pop();
        } else {
          // Inside @media etc., declarations still apply. Don't push selector
          // stack — keep the surrounding selector chain in effect.
          parseBlock(ctx, i + 1, blockEnd);
        }
      } else if (header.length > 0) {
        const resolved = resolveSelector(ctx.selectorStack, header);
        ctx.selectorStack.push(resolved);
        parseBlock(ctx, i + 1, blockEnd);
        ctx.selectorStack.pop();
      } else {
        parseBlock(ctx, i + 1, blockEnd);
      }
      i = blockEnd + 1;
      segmentStart = i;
      continue;
    }

    if (ch === "}") {
      // End of region.
      return;
    }

    if (ch === ";") {
      const segment = ctx.stripped.slice(segmentStart, i);
      handleDeclaration(ctx, segment, segmentStart);
      i++;
      segmentStart = i;
      continue;
    }

    i++;
  }
  // Trailing declaration without `;` (rare; allowed by some CSS dialects).
  const tail = ctx.stripped.slice(segmentStart, end).trim();
  if (tail.length > 0 && tail.includes(":")) {
    handleDeclaration(ctx, tail, segmentStart);
  }
}

function skipString(src: string, i: number, quote: string): number {
  i++; // past opening quote
  while (i < src.length) {
    const ch = src[i];
    if (ch === "\\") {
      i += 2;
      continue;
    }
    if (ch === quote) return i + 1;
    i++;
  }
  return i;
}

function skipBalanced(src: string, i: number, open: string, close: string): number {
  let depth = 0;
  while (i < src.length) {
    const ch = src[i];
    if (ch === '"' || ch === "'") {
      i = skipString(src, i, ch);
      continue;
    }
    if (ch === open) {
      depth++;
      i++;
      continue;
    }
    if (ch === close) {
      depth--;
      i++;
      if (depth === 0) return i;
      continue;
    }
    i++;
  }
  return i;
}

function matchBrace(src: string, openAt: number): number {
  let depth = 0;
  let i = openAt;
  while (i < src.length) {
    const ch = src[i];
    if (ch === '"' || ch === "'") {
      i = skipString(src, i, ch);
      continue;
    }
    if (ch === "(") {
      i = skipBalanced(src, i, "(", ")");
      continue;
    }
    if (ch === "{") {
      depth++;
      i++;
      continue;
    }
    if (ch === "}") {
      depth--;
      if (depth === 0) return i;
      i++;
      continue;
    }
    i++;
  }
  return -1;
}

// ---------------------------------------------------------------------------
// Declarations
// ---------------------------------------------------------------------------

function handleDeclaration(ctx: ParseContext, segment: string, segmentStartOffset: number): void {
  const trimmedStart = segment.search(/\S/);
  if (trimmedStart < 0) return;
  const text = segment.slice(trimmedStart).trimEnd();
  if (!text) return;

  // Skip @-rules (`@import "x"`, `@include foo()`, `@return $x`) — they aren't
  // declarations.
  if (text.startsWith("@")) return;

  // Skip SCSS variable assignments (`$foo: red`) — they're not CSS
  // declarations and shouldn't appear in the fact IR.
  if (text.startsWith("$")) return;

  const colon = findTopLevelColon(text);
  if (colon === -1) return;

  const property = text.slice(0, colon).trim();
  const valueText = text.slice(colon + 1).trim();
  if (!property || !valueText) return;
  const theme = ctx.themeStack[ctx.themeStack.length - 1];
  if (theme && property.startsWith("--")) {
    emitThemeDeclaration(ctx, property, segment, segmentStartOffset, trimmedStart, theme);
    return;
  }
  // Pseudo-selectors (`:hover { ... }`) have already been routed via the `{`
  // branch above; if we see one here it'd be malformed, so skip.
  if (!/^-?[a-zA-Z][\w-]*$/.test(property)) return;

  const propertyOffset =
    segmentStartOffset + trimmedStart + segment.slice(trimmedStart).indexOf(property);
  const location = locFromOffset(
    ctx.lineStarts,
    propertyOffset,
    propertyOffset + property.length,
    ctx.file
  );

  // Use the original (non-stripped) source for the value so embedded
  // characters like commas in `rgb(0, 0, 0)` survive intact.
  const valueStartInOriginal =
    segmentStartOffset + trimmedStart + segment.slice(trimmedStart).indexOf(":") + 1;
  const valueEndInOriginal = segmentStartOffset + segment.length;
  const originalValue = ctx.source.slice(valueStartInOriginal, valueEndInOriginal).trim();

  const selector = ctx.selectorStack.length ? ctx.selectorStack[ctx.selectorStack.length - 1] : "";
  const counterKey = `${selector}::${property}`;
  const occurrence = ctx.declCounters.get(counterKey) ?? 0;
  ctx.declCounters.set(counterKey, occurrence + 1);
  const declarationPath = String(occurrence);

  ctx.facts.push(
    makeStyleDeclarationFact({
      file: ctx.file,
      selector,
      declarationPath,
      property,
      value: originalValue,
      location,
    })
  );
}

function emitThemeDeclaration(
  ctx: ParseContext,
  property: string,
  segment: string,
  segmentStartOffset: number,
  trimmedStart: number,
  theme: { inline: boolean; flavor: "default" | "inline" | "reference" | "static" | "unknown" }
): void {
  const propertyOffset =
    segmentStartOffset + trimmedStart + segment.slice(trimmedStart).indexOf(property);
  const location = locFromOffset(
    ctx.lineStarts,
    propertyOffset,
    propertyOffset + property.length,
    ctx.file
  );

  const valueStartInOriginal =
    segmentStartOffset + trimmedStart + segment.slice(trimmedStart).indexOf(":") + 1;
  const valueEndInOriginal = segmentStartOffset + segment.length;
  const originalValue = ctx.source.slice(valueStartInOriginal, valueEndInOriginal).trim();

  const counterKey = `@theme::${property}`;
  const occurrence = ctx.declCounters.get(counterKey) ?? 0;
  ctx.declCounters.set(counterKey, occurrence + 1);

  ctx.facts.push(
    makeThemeDeclarationFact({
      file: ctx.file,
      name: property,
      value: originalValue,
      declarationPath: String(occurrence),
      inline: theme.inline,
      flavor: theme.flavor,
      location,
    })
  );
}

function parseThemeHeader(
  header: string
): { inline: boolean; flavor: "default" | "inline" | "reference" | "static" | "unknown" } | null {
  const match = /^@theme\b(.*)$/i.exec(header.trim());
  if (!match) return null;
  const flags = match[1].trim().split(/\s+/).filter(Boolean);
  const flavor = flags.includes("inline")
    ? "inline"
    : flags.includes("reference")
      ? "reference"
      : flags.includes("static")
        ? "static"
        : flags.length > 0
          ? "unknown"
          : "default";
  return { inline: flavor === "inline", flavor };
}

function findTopLevelColon(text: string): number {
  let depth = 0;
  for (let i = 0; i < text.length; i++) {
    const ch = text[i];
    if (ch === "(") depth++;
    else if (ch === ")") depth = Math.max(0, depth - 1);
    else if (ch === ":" && depth === 0) return i;
  }
  return -1;
}

function locFromOffset(
  lineStarts: number[],
  startOffset: number,
  endOffset: number,
  file: string
): FactLocation {
  const start = lineColFromOffset(lineStarts, startOffset);
  const end = lineColFromOffset(lineStarts, endOffset);
  return {
    file,
    line: start.line,
    column: start.column,
    endLine: end.line,
    endColumn: end.column,
  };
}

// ---------------------------------------------------------------------------
// Selector resolution
// ---------------------------------------------------------------------------

function resolveSelector(stack: string[], rawSelector: string): string {
  // Compress whitespace inside selectors so identity hashing stays stable
  // regardless of source formatting.
  const compact = rawSelector.replace(/\s+/g, " ").trim();
  if (stack.length === 0) return compact;

  const parent = stack[stack.length - 1];
  // SCSS allows multiple selectors separated by commas. Resolve each against
  // the parent and rejoin so `.a { &.b, &.c { } }` becomes `.a.b, .a.c`.
  const parts = splitTopLevel(compact, ",").map((part) => part.trim());
  const resolved = parts.map((part) => mergeSelectorPart(parent, part));
  return resolved.join(", ");
}

function mergeSelectorPart(parent: string, child: string): string {
  if (child.includes("&")) {
    return child.replace(/&/g, parent);
  }
  return `${parent} ${child}`;
}

function splitTopLevel(text: string, sep: string): string[] {
  const out: string[] = [];
  let depth = 0;
  let last = 0;
  for (let i = 0; i < text.length; i++) {
    const ch = text[i];
    if (ch === "(") depth++;
    else if (ch === ")") depth = Math.max(0, depth - 1);
    else if (ch === sep && depth === 0) {
      out.push(text.slice(last, i));
      last = i + 1;
    }
  }
  out.push(text.slice(last));
  return out;
}
