import { diagnosticCount, linter } from '@codemirror/lint';
import type { Diagnostic } from '@codemirror/lint';
import type { Extension } from '@codemirror/state';
import { EditorView } from '@codemirror/view';

/**
 * Retourne les diagnostics HTML pour un code donné.
 * Parse le contenu comme du XML strict pour détecter les erreurs de structure,
 * après normalisation des balises void HTML5 (accepte `<br>` comme `<br/>`).
 *
 * Comportement attendu :
 * - Chaîne vide → aucun diagnostic
 * - HTML bien formé (`<div><p>Hello</p></div>`) → aucun diagnostic
 * - Balises void HTML5 (`<br>`, `<meta>`, `<link>`, `<img>`…) → aucun diagnostic
 * - Balise non fermée (`<div><p>Hello</div>`) → diagnostic « HTML invalide »
 * - Attribut mal formé (`<div class=>`) → diagnostic « HTML invalide »
 */
const ROOT_TAG = '<root>';

const VOID_ELEMENTS =
  'area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr';

// Adds `=""` to boolean attributes (e.g. `data-ofos` → `data-ofos=""`).
// Skips content inside quoted attribute values to avoid false replacements.
const normalizeBooleanAttributes = (code: string): string =>
  code.replace(/<[a-zA-Z][^>]*>/g, (tag) =>
    tag.replace(
      /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(\s+)([a-zA-Z][a-zA-Z0-9-:]*)(?!\s*=)(?=[\s/>]|$)/g,
      (match, quoted, spaces, attrName) =>
        quoted !== undefined ? match : `${spaces}${attrName}=""`,
    ),
  );

const normalizeVoidElements = (code: string): string =>
  code.replace(
    new RegExp(`<(${VOID_ELEMENTS})(\\s[^>]*)?(?<!/)>`, 'gi'),
    (_, tag, attrs) => `<${tag}${attrs ?? ''}/>`,
  );

const htmlErrorOffset = (errorText: string, code: string): number => {
  const match = errorText.match(/error on line (\d+) at column (\d+)/i);
  if (!match) return 0;

  const line = parseInt(match[1]);
  // La colonne de la ligne 1 inclut le préfixe <root> — on le retire
  const col = parseInt(match[2]) - (line === 1 ? ROOT_TAG.length : 0);

  const lines = code.split('\n');
  let offset = 0;
  for (let i = 0; i < line - 1 && i < lines.length; i++) {
    offset += lines[i].length + 1;
  }
  return Math.min(Math.max(0, offset + col - 1), code.length);
};

export const htmlDiagnostics = (code: string): Diagnostic[] => {
  if (!code.trim()) return [];
  const normalized = normalizeVoidElements(normalizeBooleanAttributes(code));
  const parser = new DOMParser();
  const doc = parser.parseFromString(`${ROOT_TAG}${normalized}</root>`, 'text/xml');
  const parserError = doc.querySelector('parsererror');
  if (!parserError) return [];

  const from = htmlErrorOffset(parserError.textContent ?? '', code);
  return [
    {
      from,
      to: Math.min(from + 1, code.length),
      severity: 'error',
      message: `HTML invalide : ${parserError.textContent?.split('\n')[0] ?? 'balise mal formée ou non fermée'}`,
    },
  ];
};

/**
 * Retourne les diagnostics CSS pour un code donné.
 * Utilise l'API Constructable Stylesheets (`CSSStyleSheet.replaceSync`).
 *
 * Comportement attendu :
 * - Chaîne vide → aucun diagnostic
 * - CSS valide (`.foo { color: red; }`) → aucun diagnostic
 * - CSS invalide → diagnostic avec message du navigateur
 * - Environnements sans `CSSStyleSheet.replaceSync` (SSR, jsdom) → aucun diagnostic
 *   (la validation est silencieusement désactivée)
 */
export const cssDiagnostics = (code: string): Diagnostic[] => {
  if (!code.trim()) return [];
  try {
    const sheet = new CSSStyleSheet();
    if (typeof sheet.replaceSync !== 'function') return [];
    sheet.replaceSync(code);
    return [];
  } catch (e) {
    return [
      {
        from: 0,
        to: code.length,
        severity: 'error',
        message: `CSS invalide : ${(e as Error).message}`,
      },
    ];
  }
};

/**
 * Retourne les diagnostics JavaScript pour un code donné.
 * Détecte les erreurs de syntaxe via `new Function(code)`.
 *
 * Comportement attendu :
 * - Chaîne vide → aucun diagnostic
 * - JS syntaxiquement valide (`const x = 1;`) → aucun diagnostic
 * - Accolade mal formée (`const x = {;`) → diagnostic « JavaScript invalide »
 * - Bloc non fermé (`function foo() {`) → diagnostic « JavaScript invalide »
 *
 * Limitation : valide uniquement la syntaxe, pas la sémantique ni les types.
 * Les erreurs runtime (variable inexistante, etc.) ne sont pas détectées.
 */
export const jsDiagnostics = (code: string): Diagnostic[] => {
  if (!code.trim()) return [];
  try {
    new Function(code);
    return [];
  } catch (e) {
    return [
      {
        from: 0,
        to: code.length,
        severity: 'error',
        message: `JavaScript invalide : ${(e as Error).message}`,
      },
    ];
  }
};

/**
 * Linter syntaxique HTML pour BmsInputCode.
 * À passer via la prop `extraExtensions`, combiné avec `lintGutter()`.
 *
 * @example
 * <BmsInputCode type="html" :extra-extensions="[lintGutter(), htmlSyntaxLinter()]" />
 */
export const htmlSyntaxLinter = (): Extension =>
  linter((view) => htmlDiagnostics(view.state.doc.toString()));

/**
 * Linter syntaxique CSS pour BmsInputCode.
 * À passer via la prop `extraExtensions`, combiné avec `lintGutter()`.
 *
 * @example
 * <BmsInputCode type="css" :extra-extensions="[lintGutter(), cssSyntaxLinter()]" />
 */
export const cssSyntaxLinter = (): Extension =>
  linter((view) => cssDiagnostics(view.state.doc.toString()));

/**
 * Linter syntaxique JavaScript pour BmsInputCode.
 * À passer via la prop `extraExtensions`, combiné avec `lintGutter()`.
 *
 * @example
 * <BmsInputCode type="javascript" :extra-extensions="[lintGutter(), jsSyntaxLinter()]" />
 */
export const jsSyntaxLinter = (): Extension =>
  linter((view) => jsDiagnostics(view.state.doc.toString()));

/**
 * Extension CodeMirror qui appelle `onValidation` à chaque fois que le résultat
 * des linters change, en passant `true` si aucun diagnostic n'est présent.
 *
 * Utile pour synchroniser l'état de validation du linter avec un état Vue externe
 * (ex: désactiver un bouton de soumission).
 *
 * Comportement attendu :
 * - Appelé après chaque cycle de lint (asynchrone, après le debounce du linter)
 * - `valid: true`  → aucun diagnostic actif dans l'éditeur
 * - `valid: false` → au moins un diagnostic présent
 *
 * Note : démarre à `true` (avant le premier lint). Combiner avec une validation
 * côté formulaire (champ non vide) pour éviter de soumettre avant le premier lint.
 *
 * @example
 * const sourceValid = ref(true);
 * const extensions = [lintGutter(), htmlSyntaxLinter(), lintValidationListener((v) => { sourceValid.value = v; })];
 */
/**
 * Crée un linter qui exécute `fn` et appelle `onHasErrors` lorsque la présence
 * de diagnostics de la sévérité donnée change. À utiliser pour les linters bloquants
 * (désactiver le bouton de soumission sur `error`, ou avertir sur `warning`).
 * Pour les linters purement informatifs, utiliser `linter()` directement.
 *
 * @example
 * const htmlValid = ref(true);
 * const extensions = [
 *   lintGutter(),
 *   createLinterWithCallback(htmlDiagnostics, (hasErrors) => { htmlValid.value = !hasErrors; }, 'error'),
 * ];
 */
export const createLinterWithCallback = (
  fn: (code: string) => Diagnostic[],
  onHasDiagnostics: (hasDiagnostics: boolean) => void,
  severity: Diagnostic['severity'] = 'error',
): Extension =>
  linter((view) => {
    const diagnostics = fn(view.state.doc.toString());
    onHasDiagnostics(diagnostics.some((d) => d.severity === severity));
    return diagnostics;
  });

export const lintValidationListener = (
  onValidation: (valid: boolean) => void,
): Extension =>
  EditorView.updateListener.of((update) => {
    if (
      update.transactions.some((t) => t.effects.length > 0) ||
      update.docChanged
    ) {
      onValidation(diagnosticCount(update.state) === 0);
    }
  });

export { linter, lintGutter } from '@codemirror/lint';
