import {
  factId,
  type FactId,
  type FactLocation,
  type TailwindClassFact,
  type TailwindModifier,
  type TailwindValue,
} from "@fragments-sdk/core";

import { classifyModifier } from "./modifier-list.js";
import {
  TAILWIND_UTILITY_NAMES,
  TAILWIND_UTILITY_NAMES_LONGEST_FIRST,
} from "./utility-list.js";

export interface ParseClassInput {
  raw: string;
  file: string;
  nodeId: FactId;
  originPath: string;
  location: FactLocation;
  tailwindPrefix?: string;
}

interface ParsedClass {
  prefix: string | null;
  modifiers: TailwindModifier[];
  important: boolean;
  negative: boolean;
  utility: string;
  value: TailwindValue;
}

export function parseClass(input: ParseClassInput): TailwindClassFact {
  try {
    const parsed = parseClassShape(input.raw, input.tailwindPrefix);
    return tailwindClassFact(input, parsed);
  } catch {
    return tailwindClassFact(input, unknownClass());
  }
}

function tailwindClassFact(
  input: ParseClassInput,
  parsed: ParsedClass,
): TailwindClassFact {
  return {
    id: factId("tailwind_class", {
      nodeId: input.nodeId,
      originPath: input.originPath,
    }),
    kind: "tailwind_class",
    file: input.file,
    nodeId: input.nodeId,
    raw: input.raw,
    originPath: input.originPath,
    location: input.location,
    prefix: parsed.prefix,
    modifiers: parsed.modifiers.map((m) => ({ ...m })),
    important: parsed.important,
    negative: parsed.negative,
    utility: parsed.utility,
    value: { ...parsed.value },
  };
}

function parseClassShape(rawInput: string, tailwindPrefix?: string): ParsedClass {
  const raw = rawInput.trim();
  if (raw.length === 0) return unknownClass();

  const segments = splitOutsidePairs(raw, ":");
  const utilitySegment = segments.pop() ?? "";
  if (utilitySegment.length === 0) return unknownClass();

  const modifiers = segments
    .filter((segment) => segment.length > 0)
    .map((segment) => ({ raw: segment, kind: classifyModifier(segment) }));

  let prefix: string | null = null;
  let body = utilitySegment;
  if (tailwindPrefix && body.startsWith(tailwindPrefix)) {
    prefix = tailwindPrefix;
    body = body.slice(tailwindPrefix.length);
  }

  let important = false;
  let negative = false;
  let changed = true;
  while (changed && body.length > 0) {
    changed = false;
    if (body.startsWith("!")) {
      important = true;
      body = body.slice(1);
      changed = true;
    }
    if (body.startsWith("-")) {
      negative = true;
      body = body.slice(1);
      changed = true;
    }
  }

  // Tailwind v4 important is a trailing `!` (e.g. `text-red-500!`). Strip it so
  // the bang does not corrupt the utility token. Arbitrary values end in `]`/`)`,
  // not `!`, so this only fires on genuine trailing-important syntax.
  if (body.endsWith("!")) {
    important = true;
    body = body.slice(0, -1);
  }

  if (body.length === 0) {
    return { prefix, modifiers, important, negative, utility: "unknown", value: { kind: "none" } };
  }

  const { base, opacity } = stripOpacity(body);
  const parsedValue = parseUtilityValue(base, opacity);

  return {
    prefix,
    modifiers,
    important,
    negative,
    utility: parsedValue.utility,
    value: parsedValue.value,
  };
}

/** Sentinel utility for a standalone arbitrary property like `[mask-type:luminance]`. */
const ARBITRARY_PROPERTY_UTILITY = "[arbitrary-property]";

function parseUtilityValue(
  body: string,
  opacity: string | undefined,
): { utility: string; value: TailwindValue } {
  // Arbitrary property: a whole-class bracket group containing `property:value`
  // (e.g. `[mask-type:luminance]`, `[--cell-size:--spacing(7)]`). This is valid
  // Tailwind syntax — a CSS declaration, not a utility-with-value — so recognize
  // it rather than letting it fall through to "unknown".
  if (body.startsWith("[") && body.endsWith("]") && isBalancedTerminal(body, 0, "[", "]")) {
    const inner = body.slice(1, -1);
    if (inner.includes(":")) {
      return { utility: ARBITRARY_PROPERTY_UTILITY, value: { kind: "arbitrary", raw: inner } };
    }
  }

  const arbitrary = splitTerminalWrappedValue(body, "[", "]");
  if (arbitrary) {
    const utility = knownUtilityOrUnknown(arbitrary.utility);
    return {
      utility,
      value: utility === "unknown" ? { kind: "none" } : { kind: "arbitrary", raw: arbitrary.raw },
    };
  }

  const cssVar = splitTerminalWrappedValue(body, "(", ")");
  if (cssVar) {
    const utility = knownUtilityOrUnknown(cssVar.utility);
    return {
      utility,
      value:
        utility !== "unknown" && cssVar.raw.startsWith("--")
          ? { kind: "css-variable", ref: cssVar.raw }
          : { kind: "none" },
    };
  }

  if (TAILWIND_UTILITY_NAMES.has(body)) {
    return { utility: body, value: { kind: "none" } };
  }

  for (const utility of TAILWIND_UTILITY_NAMES_LONGEST_FIRST) {
    const prefix = `${utility}-`;
    if (!body.startsWith(prefix)) continue;
    const token = body.slice(prefix.length);
    if (token.length === 0) break;
    return {
      utility,
      value: opacity ? { kind: "token", token, opacity } : { kind: "token", token },
    };
  }

  return { utility: "unknown", value: { kind: "none" } };
}

function splitTerminalWrappedValue(
  body: string,
  open: "[" | "(",
  close: "]" | ")",
): { utility: string; raw: string } | undefined {
  if (!body.endsWith(close)) return undefined;
  const marker = `-${open}`;
  const markerIndex = findLastOutsidePairs(body, marker);
  if (markerIndex < 0) return undefined;
  const valueStart = markerIndex + marker.length;
  if (!isBalancedTerminal(body, valueStart - 1, open, close)) return undefined;
  return {
    utility: body.slice(0, markerIndex),
    raw: body.slice(valueStart, body.length - 1),
  };
}

function knownUtilityOrUnknown(utility: string): string {
  return TAILWIND_UTILITY_NAMES.has(utility) ? utility : "unknown";
}

function stripOpacity(body: string): { base: string; opacity?: string } {
  const slash = findLastTopLevelChar(body, "/");
  if (slash <= 0 || slash === body.length - 1) return { base: body };
  return { base: body.slice(0, slash), opacity: body.slice(slash + 1) };
}

function splitOutsidePairs(value: string, separator: string): string[] {
  const parts: string[] = [];
  let start = 0;
  let square = 0;
  let paren = 0;

  for (let i = 0; i < value.length; i++) {
    const ch = value[i];
    if (ch === "\\") {
      i++;
      continue;
    }
    if (ch === "[") square++;
    else if (ch === "]" && square > 0) square--;
    else if (ch === "(") paren++;
    else if (ch === ")" && paren > 0) paren--;
    else if (ch === separator && square === 0 && paren === 0) {
      parts.push(value.slice(start, i));
      start = i + separator.length;
    }
  }

  parts.push(value.slice(start));
  return parts;
}

function findLastOutsidePairs(value: string, needle: string): number {
  let square = 0;
  let paren = 0;
  let found = -1;

  for (let i = 0; i < value.length; i++) {
    const ch = value[i];
    if (ch === "\\") {
      i++;
      continue;
    }
    if (ch === "[") square++;
    else if (ch === "]" && square > 0) square--;
    else if (ch === "(") paren++;
    else if (ch === ")" && paren > 0) paren--;

    if (square === 0 && paren === 0 && value.startsWith(needle, i)) {
      found = i;
    }
  }

  return found;
}

function findLastTopLevelChar(value: string, needle: string): number {
  let square = 0;
  let paren = 0;
  let found = -1;

  for (let i = 0; i < value.length; i++) {
    const ch = value[i];
    if (ch === "\\") {
      i++;
      continue;
    }
    if (ch === "[") square++;
    else if (ch === "]" && square > 0) square--;
    else if (ch === "(") paren++;
    else if (ch === ")" && paren > 0) paren--;
    else if (ch === needle && square === 0 && paren === 0) {
      found = i;
    }
  }

  return found;
}

function isBalancedTerminal(
  value: string,
  openIndex: number,
  open: "[" | "(",
  close: "]" | ")",
): boolean {
  let depth = 0;
  for (let i = openIndex; i < value.length; i++) {
    const ch = value[i];
    if (ch === "\\") {
      i++;
      continue;
    }
    if (ch === open) depth++;
    else if (ch === close) depth--;
    if (depth === 0 && i !== value.length - 1) return false;
    if (depth < 0) return false;
  }
  return depth === 0;
}

function unknownClass(): ParsedClass {
  return {
    prefix: null,
    modifiers: [],
    important: false,
    negative: false,
    utility: "unknown",
    value: { kind: "none" },
  };
}
