import ts from "typescript";
import { factId } from "@fragments-sdk/core";
import type { SuppressionDirectiveFact } from "@fragments-sdk/core";

export interface ParseSuppressionDirectivesInput {
  filePath: string;
  source: string;
}

const EXPECT_ERROR_PATTERN =
  /@fragments-expect-error\s+(FUI\d{4})(?:\s+reason="([^"]+)")?(?:\s+expires="(\d{4}-\d{2}-\d{2})")?/;

/**
 * `fragments-allow FUI####: <free-text reason to end of comment>` — the
 * lightweight bypass flavor. The colon before the reason is mandatory; a
 * missing or malformed FUI code means no match, so the rule fires normally.
 */
const ALLOW_PATTERN = /fragments-allow\s+(FUI\d{4})\s*:([^\n]*)/;

/** Comment substrings that mark a candidate directive comment. */
const DIRECTIVE_MARKERS = ["@fragments-expect-error", "fragments-allow"] as const;

interface CommentRange {
  text: string;
  start: number;
  end: number;
}

interface ParsedDirective {
  code: string;
  reason: string;
  expiresOn?: string;
  flavor: "expect-error" | "allow";
}

export function parseSuppressionDirectives(
  input: ParseSuppressionDirectivesInput
): SuppressionDirectiveFact[] {
  const lineStarts = lineStartOffsets(input.source);
  const ranges = collectDirectiveComments(input.source);
  const facts: SuppressionDirectiveFact[] = [];
  const seen = new Set<string>();

  for (const range of ranges) {
    const parsed = parseDirectiveText(range.text);
    if (!parsed) continue;

    const location = offsetLocation(lineStarts, range.start);
    const attachedLine = nextSignificantLine(input.source, lineStarts, range.end);
    const { code, reason, expiresOn } = parsed;
    const id = factId("suppression_directive", {
      file: input.filePath,
      line: location.line,
      column: location.column,
      code,
      reason,
      expiresOn,
    });
    if (seen.has(id)) continue;
    seen.add(id);
    facts.push({
      id,
      kind: "suppression_directive",
      file: input.filePath,
      line: location.line,
      column: location.column,
      code,
      reason,
      ...(expiresOn ? { expiresOn } : {}),
      ...(attachedLine ? { attachedLine } : {}),
      ...(parsed.flavor === "allow" ? { directive: "allow" as const } : {}),
    });
  }

  return facts;
}

function parseDirectiveText(text: string): ParsedDirective | null {
  const expectError = EXPECT_ERROR_PATTERN.exec(text);
  if (expectError) {
    return {
      code: expectError[1],
      reason: expectError[2]?.trim() ?? "",
      expiresOn: expectError[3],
      flavor: "expect-error",
    };
  }

  const allow = ALLOW_PATTERN.exec(text);
  if (allow) {
    // Free-text reason runs to the end of the comment; trim a block-comment
    // terminator so `/* fragments-allow FUI4001: reason */` parses cleanly.
    const reason = allow[2].replace(/\*\/\s*$/, "").trim();
    return { code: allow[1], reason, flavor: "allow" };
  }

  return null;
}

function containsDirectiveMarker(text: string): boolean {
  return DIRECTIVE_MARKERS.some((marker) => text.includes(marker));
}

function collectDirectiveComments(source: string): CommentRange[] {
  const ranges: CommentRange[] = [];
  const sourceFile = ts.createSourceFile(
    "source.tsx",
    source,
    ts.ScriptTarget.Latest,
    true,
    ts.ScriptKind.TSX
  );

  const addLeadingRanges = (pos: number): void => {
    for (const range of ts.getLeadingCommentRanges(source, pos) ?? []) {
      if (!containsDirectiveMarker(source.slice(range.pos, range.end))) {
        continue;
      }
      ranges.push({
        text: source.slice(range.pos, range.end),
        start: range.pos,
        end: range.end,
      });
    }
  };

  const visit = (node: ts.Node): void => {
    addLeadingRanges(node.pos);
    ts.forEachChild(node, visit);
  };
  visit(sourceFile);

  const blockPattern = /\/\*[\s\S]*?(?:@fragments-expect-error|fragments-allow)[\s\S]*?\*\//g;
  for (const match of source.matchAll(blockPattern)) {
    if (match.index === undefined) continue;
    ranges.push({
      text: match[0],
      start: match.index,
      end: match.index + match[0].length,
    });
  }

  const linePattern = /\/\/[^\n]*(?:@fragments-expect-error|fragments-allow)[^\n]*/g;
  for (const match of source.matchAll(linePattern)) {
    if (match.index === undefined) continue;
    ranges.push({
      text: match[0],
      start: match.index,
      end: match.index + match[0].length,
    });
  }

  return ranges.sort((a, b) => a.start - b.start);
}

function lineStartOffsets(source: string): number[] {
  const starts = [0];
  for (let i = 0; i < source.length; i++) {
    if (source[i] === "\n") starts.push(i + 1);
  }
  return starts;
}

function offsetLocation(
  lineStarts: readonly number[],
  offset: number
): { line: number; column: number } {
  let low = 0;
  let high = lineStarts.length - 1;
  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    if (lineStarts[mid] <= offset) low = mid + 1;
    else high = mid - 1;
  }
  const lineIndex = Math.max(0, low - 1);
  return { line: lineIndex + 1, column: offset - lineStarts[lineIndex] };
}

function nextSignificantLine(
  source: string,
  lineStarts: readonly number[],
  offset: number
): number | undefined {
  const startLine = offsetLocation(lineStarts, offset).line;
  const lines = source.split(/\r?\n/);
  for (let i = startLine; i < lines.length; i++) {
    const trimmed = lines[i].trim();
    if (!trimmed) continue;
    if (trimmed.startsWith("//")) continue;
    if (trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.endsWith("*/")) {
      continue;
    }
    return i + 1;
  }
  return undefined;
}
