// gql.jsx — le pont entre le prototype et le vrai moteur.
//
// Chargé AVANT data.jsx : il installe `G4M`, un petit client GraphQL et des
// hooks React. Les écrans passent des mocks aux vraies données en changeant
// une seule ligne (`const x = MOCK` → `const x = useG4M(...)`), ce qui garde
// le prototype navigable même quand le backend dort.
//
// Trois partis pris :
//  1. **Dégradation, pas plantage.** Si le backend est absent, le hook rend le
//     mock et le signale (bandeau « mode démonstration »). Un prototype qui
//     explose en démo ne se montre à personne.
//  2. **Aucune donnée en dur ici.** Ce fichier ne connaît que la forme des
//     requêtes ; le contenu vient du serveur ou du mock d'origine.
//  3. **Le tenant n'est jamais envoyé par le client.** On transmet l'identité
//     (courriel de session), le serveur en déduit le tenant. Un front qui
//     choisirait son tenant ne serait pas cloisonné.

const G4M = (() => {
  const ENDPOINT = (window.G4M_ENDPOINT || "/graphql");
  const SESSION_KEY = "grant4me.session.email";

  const TOKEN_KEY = "grant4me.session.token";
  const state = {
    online: null,          // null = pas encore testé
    email: localStorage.getItem(SESSION_KEY) || "",
    token: localStorage.getItem(TOKEN_KEY) || "",
    lastError: null,
  };

  async function call(field, query, variables = {}) {
    const res = await fetch(ENDPOINT, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        // Le jeton prouve l'identité ; le courriel nu n'est qu'un mode LAB.
        ...(state.token ? { "Authorization": "Bearer " + state.token }
           : state.email ? { "X-User-Email": state.email } : {}),
      },
      body: JSON.stringify({ query, variables, operation: field }),
    });
    const body = await res.json().catch(() => ({}));
    if (!res.ok || body.errors) {
      const msg = (body.errors && body.errors[0] && body.errors[0].message) || `HTTP ${res.status}`;
      throw new Error(msg);
    }
    return body.data ? body.data[field] : null;
  }

  // Le champ racine suffit à notre exécuteur ; la requête reste écrite en
  // entier pour qu'elle documente ce qu'on attend (et qu'elle marche telle
  // quelle contre un vrai serveur GraphQL le jour où on en met un).
  const Q = {
    me: `query { me { userId email fullName role tenantId companyName } }`,
    company: `query { company { id legalName neq sector city adminRegion numEmployees legalForm bnFederal scianCode annualRevenueRange address productsOffered growthObjectives dateConstitution contactName contactTitle contactPhone email financingNeeds activityCodes profileComplete } }`,
    projects: `query { projects { id title description sector formDomain startDate endDate totalValue requestedAmount problemStatement objectives methodology expectedResults companyName } }`,
    runs: `query($limit:Int){ runs(limit:$limit){ id projectId projectTitle companyName status progressPct createdAt detectedFields pctAutoFilled pctHumanValidated } }`,
    run: `query($id:ID!){ run(id:$id){ id status progressPct detectedFields error resultUrl
            missingFields { fieldName question fieldType suggestedValue whyMissing choices }
            reviewFields { fieldName label value origin fieldType choices }
            requiredDocs { label description mandatory sourceInForm }
            pctAutoFilled pctHumanValidated } }`,
    formDomains: `query { formDomains { slug labels lexicon defaultLang dateFormat status referencePrograms } }`,
    activeDomain: `query($projectId:ID){ activeDomain(projectId:$projectId){ slug labels lexicon defaultLang dateFormat status referencePrograms } }`,
    project: `query($id:ID!){ project(id:$id){ id title description sector problemStatement objectives methodology expectedResults startDate endDate totalValue requestedAmount companyName } }`,
    caeSearch: `query($q:String!){ caeSearch(q:$q){ code libelle } }`,
    platformUsage: `query { platformUsage { tenantId companyName month runs runsCompleted saasUnits fields questions memoryHits llmCalls llmTokensIn llmTokensOut avgDurationMs } }`,
    saveCompany: `mutation($id:ID!,$input:JSON!){ saveCompany(id:$id,input:$input){ id legalName sector city profileComplete } }`,
    saveProject: `mutation($id:ID!,$input:JSON!){ saveProject(id:$id,input:$input){ id title objectives } }`,
    createProject: `mutation($title:String,$description:String){ createProject(title:$title,description:$description){ id title } }`,
    startRun: `mutation($projectId:ID!,$sourcePdfBase64:String!,$lang:String,$formDomain:String){
                 startRun(projectId:$projectId,sourcePdfBase64:$sourcePdfBase64,lang:$lang,formDomain:$formDomain){ id status progressPct } }`,
    provideAnswers: `mutation($runId:ID!,$answers:JSON!,$validated:[String!]){
                 provideAnswers(runId:$runId,answers:$answers,validated:$validated){ id status progressPct } }`,
    login: `mutation($email:String!,$password:String!){ login(email:$email,password:$password){ token me { userId email fullName role tenantId companyName } } }`,
    logout: `mutation($token:String){ logout(token:$token) }`,
    confirmReview: `mutation($runId:ID!,$corrections:JSON,$validated:[String!]!){
                 confirmReview(runId:$runId,corrections:$corrections,validated:$validated){ id status progressPct } }`,
  };

  const api = {
    get email() { return state.email; },
    setEmail(e) {
      state.email = e || "";
      try { localStorage.setItem(SESSION_KEY, state.email); } catch (err) {}
    },
    get online() { return state.online; },
    get lastError() { return state.lastError; },

    async probe() {
      try {
        await call("me", Q.me);
        state.online = true;
      } catch (e) {
        state.online = false;
        state.lastError = e.message;
      }
      return state.online;
    },

    async login(email, password) {
      const r = await call("login", Q.login, { email, password });
      state.token = r.token; api.setEmail(email);
      try { localStorage.setItem(TOKEN_KEY, r.token); } catch (e) {}
      return r.me;
    },
    async logout() {
      try { await call("logout", Q.logout, { token: state.token }); } catch (e) {}
      state.token = "";
      try { localStorage.removeItem(TOKEN_KEY); } catch (e) {}
    },
    get token() { return state.token; },
    me: () => call("me", Q.me),
    company: () => call("company", Q.company),
    projects: () => call("projects", Q.projects),
    runs: (limit = 20) => call("runs", Q.runs, { limit }),
    run: (id) => call("run", Q.run, { id }),
    formDomains: () => call("formDomains", Q.formDomains),
    activeDomain: (projectId) => call("activeDomain", Q.activeDomain, { projectId: projectId || null }),
    project: (id) => call("project", Q.project, { id }),
    caeSearch: (q) => call("caeSearch", Q.caeSearch, { q }),
    platformUsage: () => call("platformUsage", Q.platformUsage),
    saveCompany: (id, input) => call("saveCompany", Q.saveCompany, { id, input }),
    saveProject: (id, input) => call("saveProject", Q.saveProject, { id, input }),
    createProject: (title, description) => call("createProject", Q.createProject, { title, description }),
    startRun: (v) => call("startRun", Q.startRun, v),
    provideAnswers: (v) => call("provideAnswers", Q.provideAnswers, v),
    confirmReview: (v) => call("confirmReview", Q.confirmReview, v),

    /** Lit un PDF choisi dans un <input type=file> en base64 (sans le préfixe
     *  data:), la forme que l'API du moteur attend. */
    fileToBase64(file) {
      return new Promise((resolve, reject) => {
        const r = new FileReader();
        r.onload = () => resolve(String(r.result).split(",")[1] || "");
        r.onerror = reject;
        r.readAsDataURL(file);
      });
    },
  };

  return api;
})();

/** Hook : données réelles si le backend répond, mock sinon.
 *  Retourne [valeur, meta] où meta = { loading, live, error }. */
function useG4M(fetcher, fallback, deps = []) {
  const [value, setValue] = React.useState(fallback);
  const [meta, setMeta] = React.useState({ loading: true, live: false, error: null });
  React.useEffect(() => {
    let alive = true;
    (async () => {
      try {
        const v = await fetcher();
        if (!alive) return;
        // Une liste vide côté serveur reste une réponse valide : on n'invente
        // pas des données pour « faire joli ».
        setValue(v == null ? fallback : v);
        setMeta({ loading: false, live: true, error: null });
      } catch (e) {
        if (!alive) return;
        setValue(fallback);
        setMeta({ loading: false, live: false, error: e.message });
      }
    })();
    return () => { alive = false; };
  }, deps);
  return [value, meta];
}

/** Suit un run jusqu'à une étape où l'humain reprend la main. Le polling
 *  s'arrête de lui-même : un onglet oublié ne doit pas marteler le serveur. */
function useRunPolling(runId, { intervalMs = 2000, onPhase } = {}) {
  const [run, setRun] = React.useState(null);
  const [err, setErr] = React.useState(null);
  React.useEffect(() => {
    if (!runId) return;
    let alive = true, timer = null;
    const TERMINAL = ["awaiting_user", "awaiting_review", "done", "failed"];
    const tick = async () => {
      try {
        const r = await G4M.run(runId);
        if (!alive) return;
        setRun(r);
        if (onPhase && r) onPhase(r);
        if (r && TERMINAL.includes(r.status)) return;
      } catch (e) {
        if (!alive) return;
        setErr(e.message);
      }
      timer = setTimeout(tick, intervalMs);
    };
    tick();
    return () => { alive = false; if (timer) clearTimeout(timer); };
  }, [runId]);
  return [run, err];
}

/** Recherche CAE contre le VRAI référentiel, avec repli sur le jeu embarqué
 *  si le backend dort. Débounce court : on interroge la base à la frappe, pas
 *  à chaque touche. */
function useCaeSearch() {
  const cache = React.useRef({});
  const [, force] = React.useState(0);
  return React.useCallback((q) => {
    const key = (q || "").trim().toLowerCase();
    if (!key) return [];
    if (cache.current[key]) return cache.current[key];
    cache.current[key] = [];            // évite les appels en rafale
    G4M.caeSearch(key)
      .then((rows) => { cache.current[key] = rows || []; force((n) => n + 1); })
      .catch(() => { cache.current[key] = (typeof searchCae === "function" ? searchCae(key) : []); force((n) => n + 1); });
    return typeof searchCae === "function" ? searchCae(key) : [];
  }, []);
}

/** Bandeau discret : dit la vérité sur l'origine de ce qui est affiché.
 *  Un écran de démonstration qui se fait passer pour du réel est un piège
 *  qu'on se tend à soi-même. */
function LiveBadge({ meta }) {
  if (!meta || meta.loading) return null;
  if (meta.live) {
    return <span className="pill pill-green pill-sm" title={I18N.t("données réelles")}>
      <Icon name="check" size={12} strokeWidth={2.6} /> {I18N.t("données réelles")}</span>;
  }
  return <span className="pill pill-muted pill-sm"
    title={meta.error || I18N.t("mode démonstration")}>
    {I18N.t("mode démonstration")}</span>;
}
