// src/lib/keywords/import/detect-file.ts — PURE file-kind detector for the
// unified "Sources & IA" import dropzone. No I/O, deterministic.
//
// Composes the EXISTING detectors so the dropzone can route a dropped file:
//   - keyword-CSV header inspection — mirrors `detectSource()` in
//     `src/components/keywords/import-keywords-dialog.tsx` (Mangools / Semrush /
//     Ahrefs / SE-Ranking header tokens, also fed to the WASM/DuckDB engine).
//   - YTG/Lumenia `.txt` markers — same family of markers used by
//     `isBriefFile()` / `isGuideFile()` in `src/lib/keywords/ytg-parse.ts`.
//   - SERP "URLs" export sniff — same url + position header probe as
//     `importSerpSnapshot()` in `src/lib/keywords/serp-store.ts`.
import type { KeywordSource } from "../types";

/** What kind of file the dropzone is looking at. `source` on a keyword-csv is
 *  the detected provider, or "unknown" when it is clearly a keyword/SEO CSV
 *  whose provider we don't recognise. */
export type ImportFileKind =
  | { kind: "keyword-csv"; source: KeywordSource | "unknown" }
  | { kind: "ytg-brief" }
  | { kind: "serp-csv" }
  | { kind: "unknown" };

/** Strip a leading UTF-8 BOM (some exports — ranxplorer, Screaming Frog — emit
 *  one). Both the literal ﻿ char and the ﻿ code point are handled. */
function stripBom(s: string): string {
  return s.replace(/^﻿/, "").replace(/^﻿/, "");
}

/** First non-empty line of a text sample (the CSV header row). */
export function firstLine(headSample: string): string {
  for (const raw of headSample.split(/\r?\n/)) {
    const line = stripBom(raw);
    if (line.trim() !== "") return line;
  }
  return "";
}

/** True when the filename / sample looks like a `.txt` (not a CSV). */
function isTxtName(filename: string): boolean {
  return /\.txt$/i.test(filename.trim());
}

/** True when the filename / sample looks like a delimited CSV-ish export. */
function isCsvName(filename: string): boolean {
  return /\.(csv|tsv)$/i.test(filename.trim());
}

/** YTG / Lumenia brief or guide `.txt`. Mirrors the marker family used by
 *  `parseYtgTexts` (Lumenia.io header, SEO Brief / SEO GUIDE, TOP TERMS). The
 *  "Lumenia.io" banner is the most robust single marker; the others catch
 *  re-saved / partial files. */
export function isYtgText(sample: string): boolean {
  const t = stripBom(sample);
  return (
    t.includes("Lumenia.io") ||
    t.includes("SEO Brief") ||
    t.includes("SEO GUIDE") ||
    t.includes("== TOP TERMS ==") ||
    t.includes("Context and Objective")
  );
}

/** Best-effort keyword-CSV provider detection from the header line. Copies the
 *  heuristics of `detectSource()` in import-keywords-dialog.tsx 1:1. Returns a
 *  concrete KeywordSource or null when the header isn't a known provider. */
export function detectKeywordSource(header: string): KeywordSource | null {
  const h = stripBom(header);
  if (h.includes("Avg. Search Volume (Last Known Values)")) return "mangools";
  if (h.includes("Competitive Density") && h.includes("Number of Results"))
    return "semrush";
  // Real Semrush "Positions" export header uses "Competition" + "Number of
  // Results" + "Keyword Difficulty" (the WASM/DuckDB engine accepts both
  // layouts); keep the dialog's primary token but also recognise this variant.
  if (
    h.includes("Keyword Difficulty") &&
    h.includes("Number of Results") &&
    h.includes("Keyword Intents")
  )
    return "semrush";
  if (h.includes("Traffic potential") && h.includes("Difficulty"))
    return "ahrefs";
  if (h.includes("Search vol.") && h.includes("Search intent"))
    return "seranking";
  return null;
}

/** Heuristic: does this header look like a *keyword-list* export (a term column
 *  plus at least one SEO metric column), even if the provider is unknown? Used
 *  so an unrecognised-but-keyword CSV still routes to the keyword importer
 *  (source "unknown") instead of being dropped as fully unknown.
 *
 *  Deliberately conservative: it rejects headers that carry a `url`/`host`
 *  column. Those are position/crawl/SERP-style exports (ranxplorer "positions",
 *  Screaming Frog on-page) the importer can't ingest yet — the spec marks them
 *  "format pas encore supporté", so they must fall through to `unknown`.
 *  Known providers (Semrush organic, which has its own URL column) are matched
 *  earlier by `detectKeywordSource`, so excluding `url` here is safe. */
export function looksLikeKeywordCsv(header: string): boolean {
  const h = stripBom(header).toLowerCase();
  const hasUrlColumn = /(^|[,;\t])\s*"?(url|host|adresse)"?\s*([,;\t]|$)/.test(
    h,
  );
  if (hasUrlColumn) return false;
  const hasTerm =
    /(^|[,;\t])\s*"?(keyword|mot|mot-clé|mot clé|query|term)"?\s*([,;\t]|$)/.test(
      h,
    );
  const hasMetric =
    /search\s*vol/.test(h) ||
    /volume(\s*de)?\s*recherche/.test(h) ||
    /volume\s*recherche/.test(h) ||
    /search\s*volume/.test(h) ||
    /\bcpc\b/.test(h) ||
    /difficult/.test(h) || // Difficulty / Difficulté / Kd
    /\bkd\b/.test(h) ||
    /search\s*intent/.test(h) ||
    /keyword\s*intents/.test(h);
  return hasTerm && hasMetric;
}

/** SERP "URLs" export sniff — header contains both a `url` and a `position`
 *  column (SE-Ranking / SerpChecker layout). Same probe as `importSerpSnapshot`
 *  in serp-store.ts, scanning the first lines past any metric preamble.
 *
 *  A genuine SERP-URLs export is the result set for ONE query, so it has no
 *  keyword/term column; rank-tracking exports that pair position+url with a
 *  per-row term (ranxplorer "positions") are excluded — they aren't SERP
 *  snapshots and the spec marks them unsupported. */
export function looksLikeSerpCsv(sample: string): boolean {
  return stripBom(sample)
    .split(/\r?\n/, 12)
    .some((raw) => {
      const hasUrl = /(^|[,;\t])\s*"?url"?\s*([,;\t]|$)/i.test(raw);
      const hasPosition = /(^|[,;\t])\s*"?position"?\s*([,;\t]|$)/i.test(raw);
      const hasTerm =
        /(^|[,;\t])\s*"?(keyword|mot|mot-clé|mot clé|query|term)"?\s*([,;\t]|$)/i.test(
          raw,
        );
      return hasUrl && hasPosition && !hasTerm;
    });
}

/**
 * Detect what kind of import file we're looking at, from the filename and the
 * first ~64 KB of its text (read by the caller). Pure + deterministic.
 *
 * Precedence:
 *  1. `.txt` with YTG/Lumenia markers → `ytg-brief`.
 *  2. CSV matching a known keyword-CSV provider header → `keyword-csv` (source).
 *  3. CSV that looks like a keyword/SEO export but provider unrecognised →
 *     `keyword-csv` (source "unknown").
 *  4. Sample matching a SERP `url`+`position` header → `serp-csv`.
 *  5. Otherwise → `unknown`.
 */
export function detectImportFile(
  filename: string,
  headSample: string,
): ImportFileKind {
  // 1. YTG brief/guide text.
  if (isTxtName(filename) && isYtgText(headSample)) {
    return { kind: "ytg-brief" };
  }

  const header = firstLine(headSample);
  const csvLike = isCsvName(filename) || !isTxtName(filename);

  if (csvLike) {
    // 2. Known keyword-CSV provider.
    const source = detectKeywordSource(header);
    if (source) return { kind: "keyword-csv", source };

    // 3. Unrecognised provider but clearly a keyword/SEO export.
    //    (Checked before SERP so a keyword CSV that happens to carry a `url`
    //    column isn't mis-routed.)
    if (looksLikeKeywordCsv(header)) {
      return { kind: "keyword-csv", source: "unknown" };
    }

    // 4. SERP URLs export.
    if (looksLikeSerpCsv(headSample)) {
      return { kind: "serp-csv" };
    }
  }

  // 5. Anything else (Screaming Frog on-page crawl, ranxplorer evolution
  //    exports, random .txt, …).
  return { kind: "unknown" };
}
