import { Platform } from "react-native";
import Constants from "expo-constants";
import { Directory, File, Paths } from "expo-file-system";
import apiClient, { ApiClientError } from "../services/apiClient";

// Collecteur de crashs "maison" : zero dependance externe, zero service tiers.
// Les incidents sont persistes localement (anneau borne en JSONL) puis pousses
// au backend (best-effort, tolerant 404) au prochain demarrage/reconnexion.
// Voir OBSERVABILITE_CRASH.md (approche A).

export type CrashKind = "render" | "fatal" | "rejection";

// Contexte non sensible attache a chaque incident. JAMAIS de token ni de donnee
// client ici (cf. garde-fous OBSERVABILITE_CRASH.md §6).
export type CrashContext = {
  instanceUrl?: string | null;
  tenantId?: string | number | null;
};

export type CrashEntry = {
  timestamp: string; // ISO 8601
  kind: CrashKind;
  message: string;
  stack?: string;
  isFatal?: boolean;
  componentStack?: string;
  app: string;
  platform: string;
  instanceUrl: string | null;
  tenantId: string | number | null;
};

export type CrashInput = {
  kind: CrashKind;
  message: string;
  stack?: string;
  isFatal?: boolean;
  componentStack?: string;
};

// Anneau borne : on ne garde que les N derniers incidents -> pas de croissance
// disque illimitee. Les stacks sont tronquees pour eviter des fichiers enormes.
const MAX_ENTRIES = 50;
const MAX_STACK_CHARS = 4000;

const APP_VERSION = Constants.expoConfig?.version ?? "?";

const crashDir = new Directory(Paths.document, "crash-logs");
const crashFile = new File(crashDir, "crashes.jsonl");

let context: CrashContext = {};

// Renseigne par App une fois l'instance/tenant connus, pour situer les incidents.
export function setCrashContext(next: Readonly<CrashContext>): void {
  context = { ...context, ...next };
}

function ensureDir(): void {
  if (!crashDir.exists) {
    crashDir.create({ intermediates: true, idempotent: true });
  }
}

function truncate(value: string | undefined): string | undefined {
  if (!value) {
    return undefined;
  }
  return value.length > MAX_STACK_CHARS ? value.slice(0, MAX_STACK_CHARS) : value;
}

// Lecture synchrone tolerante : tout echec d'I/O ou de parsing renvoie [] plutot
// que de propager. Une ligne JSONL corrompue est ignoree, pas bloquante.
function readEntriesSafe(): CrashEntry[] {
  try {
    if (!crashFile.exists) {
      return [];
    }
    return crashFile
      .textSync()
      .split("\n")
      .map((line) => line.trim())
      .filter(Boolean)
      .map((line) => {
        try {
          return JSON.parse(line) as CrashEntry;
        } catch {
          return null;
        }
      })
      .filter((entry): entry is CrashEntry => entry !== null);
  } catch {
    return [];
  }
}

// Enregistre un incident. NE LANCE JAMAIS : un logger de crash qui crashe est
// pire que pas de logger. Toute erreur d'I/O est avalee silencieusement.
export async function recordCrash(input: Readonly<CrashInput>): Promise<void> {
  try {
    const entry: CrashEntry = {
      timestamp: new Date().toISOString(),
      kind: input.kind,
      message: input.message,
      stack: truncate(input.stack),
      isFatal: input.isFatal,
      componentStack: truncate(input.componentStack),
      app: APP_VERSION,
      platform: `${Platform.OS} ${Platform.Version}`,
      instanceUrl: context.instanceUrl ?? null,
      tenantId: context.tenantId ?? null,
    };
    const next = [...readEntriesSafe(), entry].slice(-MAX_ENTRIES);
    ensureDir();
    crashFile.write(`${next.map((item) => JSON.stringify(item)).join("\n")}\n`);
  } catch {
    // I/O indisponible : on perd cet incident, mais l'app ne tombe pas a cause du logger.
  }
}

export async function readCrashLogs(): Promise<CrashEntry[]> {
  return readEntriesSafe();
}

// Marqueur present dans le message ET la stack des incidents de TEST. Permet au
// SuperAdmin de les reconnaitre au premier coup d'oeil dans la vue "Crashs
// mobile" et de les ignorer/supprimer sans risque : un test ne doit jamais etre
// pris pour un vrai incident.
export const TEST_CRASH_MARKER = "🧪 [TEST]";

export async function clearCrashLogs(): Promise<void> {
  try {
    if (crashFile.exists) {
      crashFile.delete();
    }
  } catch {
    // Suppression best-effort : sans incidence si le fichier est deja absent/verrouille.
  }
}

// Endpoints d'ingestion backend, cascade .php puis sans extension (meme convention
// que authApi). Contrat backend a implementer : voir OBSERVABILITE_CRASH.md §4 et
// la passation backend. Tant qu'il n'existe pas, l'envoi recoit 404 -> les logs
// sont CONSERVES localement (aucune perte, aucune regression).
const CRASH_ENDPOINTS = ["/mobile/crash_report.php", "/mobile/crash_report"];

async function postCrashReports(body: unknown): Promise<void> {
  for (let index = 0; index < CRASH_ENDPOINTS.length - 1; index += 1) {
    try {
      await apiClient.post(CRASH_ENDPOINTS[index], body);
      return;
    } catch (error) {
      const is404 = error instanceof ApiClientError && error.status === 404;
      if (!is404) {
        throw error;
      }
    }
  }
  await apiClient.post(CRASH_ENDPOINTS[CRASH_ENDPOINTS.length - 1], body);
}

// Pousse les incidents en attente vers le backend. Best-effort : tout echec
// (404 endpoint non deploye, hors-ligne, 401 non authentifie) CONSERVE les logs
// pour un prochain essai. NE LANCE JAMAIS. Vide le journal local seulement apres
// un envoi reussi. Renvoie le nombre d'incidents transmis. Declenche par
// syncOnReconnect (boot / reconnexion / retour foreground).
export async function flushCrashReports(): Promise<number> {
  const reports = readEntriesSafe();
  if (reports.length === 0) {
    return 0;
  }
  try {
    await postCrashReports({ reports });
    await clearCrashLogs();
    return reports.length;
  } catch {
    return 0;
  }
}

// Diagnostic MANUEL (bouton cache de l'ecran Support) : enregistre un incident
// clairement marque comme test (TEST_CRASH_MARKER) puis tente l'envoi par le
// chemin de prod exact (flushCrashReports). Sert a verifier la chaine complete
// mobile -> backend -> SuperAdmin sans attendre un vrai crash. Renvoie le nombre
// d'incidents transmis : 0 si hors-ligne / backend indisponible, auquel cas
// l'incident reste en local et sera reessaye au prochain sync, comme un vrai
// crash. NE LANCE JAMAIS (recordCrash et flushCrashReports avalent leurs erreurs).
export async function sendTestCrashReport(): Promise<number> {
  await recordCrash({
    kind: "fatal",
    message: `${TEST_CRASH_MARKER} Remontee de crash declenchee manuellement (diagnostic) — ce n'est pas un incident reel`,
    stack: `${TEST_CRASH_MARKER} Stack factice de test\n  at SupportScreen.diagnostic`,
    isFatal: false,
  });
  return flushCrashReports();
}
