// AI Recommendations — ports the Streamlit Gemini-powered recommendations
// feature. Tries Gemini REST first (if GEMINI_API_KEY is set in env.js), then
// falls back to the built-in window.claude.complete() helper so the panel
// works even without an external key.

const { useState } = React;

// Per-dimension class name — drives a soft-tinted pill that matches the
// rest of the design system (see .ai-rec-chip.dim.* in styles.css).
const DIM_SLUG = {
  Accuracy: "accuracy",
  Completeness: "completeness",
  Consistency: "consistency",
  Timeliness: "timeliness",
};

function aiThreshold() {
  return (typeof window !== "undefined" && Number(window.AI_THRESHOLD)) || 85.0;
}

// Gate the direct-Gemini-call path to localhost. A key in env.js is sent to
// the browser in plaintext — if the user deploys this app to Cloudflare /
// Netlify / GitHub Pages without clearing env.js, every visitor sees the key
// in the static bundle. Production must use the /api/gemini proxy instead.
function _isLocalhost() {
  if (typeof window === "undefined") return false;
  const h = window.location.hostname || "";
  return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "" || h.endsWith(".local");
}

// Cache-key fingerprint — cheap hash of the dataset's shape + score so a
// re-upload with the same filename but different content invalidates the
// cached recommendations. Uses row count, column count, rounded DQI, and a
// hash of the first + last row keys/values.
function _aiCacheKey(entry) {
  const df = entry.df || [];
  const rows = df.length;
  const cols = rows ? Object.keys(df[0]).length : 0;
  const dqiRounded = entry.dqi != null ? Math.round(entry.dqi * 10) : 0;
  let h = 0;
  const sampleStr = JSON.stringify([df[0] || null, df[rows - 1] || null]);
  for (let i = 0; i < sampleStr.length; i++) {
    h = ((h << 5) - h + sampleStr.charCodeAt(i)) | 0;
  }
  return `${entry.filename}::${entry.profile.name}::${rows}x${cols}@${dqiRounded}#${(h >>> 0).toString(36)}`;
}

function hasFailingCheck(entry) {
  const detailed = entry.detailed || {};
  const thr = aiThreshold();
  for (const dim of Object.values(detailed)) {
    for (const c of dim.checks || []) {
      if (c.score < thr) return true;
    }
  }
  return false;
}

// Build the same prompt the Streamlit version sends. Keeping the exact JSON
// schema means downstream rendering is identical.
function buildAiPrompt(entry) {
  const thr = aiThreshold();
  const profileName = entry.profile.name;
  const geoInfo = entry.geo_type !== "N/A" ? `${entry.geo_type}: ${entry.zone_label}` : "N/A";

  const dimLines = DIMENSIONS.map((dim) => {
    const dimScore = entry.scores[dim];
    const checks = entry.detailed?.[dim]?.checks || [];
    const failing = checks.filter((c) => c.score < thr);
    let line = `${DIM_LABELS[dim]}: ${dimScore.toFixed(1)}%`;
    if (failing.length) {
      const bullets = failing.map((c) => `    - [${c.type}] ${c.description} — score ${c.score.toFixed(1)}%`).join("\n");
      line += ` (failing checks):\n${bullets}`;
    } else {
      line += " (no failing checks)";
    }
    return line;
  });

  return `You are a data quality expert reviewing a banking dataset report.

Dataset: ${profileName}
Geography: ${geoInfo}
Overall DQI Score: ${entry.dqi.toFixed(1)}%

The following checks are failing (score below ${thr.toFixed(0)}%):
${dimLines.join("\n")}

Return a JSON array — one object per failing check — with exactly these keys:
- "dimension": the dimension name (Accuracy / Completeness / Consistency / Timeliness)
- "check": the check description
- "type": the check type identifier
- "field": the column or data field involved (e.g. "sanction_date", "all columns", etc.)
- "reason": one plain-English sentence explaining what data problem this means — no jargon
- "recommendations": a single string with 2-3 numbered steps separated by newlines (e.g. "1. ... \\n2. ... \\n3. ...")

Output raw JSON only. No markdown fences, no explanation.`;
}

// Strip ```json fences a model may add despite being told not to.
function stripFences(raw) {
  let s = (raw || "").trim();
  if (s.startsWith("```")) {
    s = s.replace(/^```[a-z]*\n?/i, "");
    s = s.replace(/\n?```\s*$/, "");
  }
  return s.trim();
}

// --- Gemini REST call -------------------------------------------------------
async function callGemini(apiKey, prompt) {
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${encodeURIComponent(apiKey)}`;
  const res = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      contents: [{ parts: [{ text: prompt }] }],
      generationConfig: { responseMimeType: "application/json" },
    }),
  });
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`Gemini API ${res.status}: ${text.slice(0, 200)}`);
  }
  const data = await res.json();
  const txt = data?.candidates?.[0]?.content?.parts?.map((p) => p.text).join("") || "";
  return txt;
}

// --- Cloudflare Pages Function proxy ---------------------------------------
// Same-origin POST to /api/gemini. The Function holds the key as a server-side
// secret, so the browser never sees it. Returns the model text.
async function callProxy(prompt) {
  const res = await fetch("/api/gemini", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ prompt }),
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    throw new Error(data?.error || `Proxy returned ${res.status}`);
  }
  return data?.text || "";
}

// Probe — checks whether /api/gemini is wired up and has its secret set.
// Used to drive the "no AI provider" warning and disable the Generate button.
// Returns { available: bool, model: string } or null if the endpoint doesn't
// exist (404 — local dev, plain static host, etc.).
async function probeProxy() {
  try {
    const res = await fetch("/api/gemini");
    if (!res.ok) return null;
    return await res.json();
  } catch {
    return null;
  }
}

// --- Claude built-in fallback ----------------------------------------------
async function callClaude(prompt) {
  if (typeof window === "undefined" || !window.claude || typeof window.claude.complete !== "function") {
    throw new Error("Built-in Claude helper unavailable.");
  }
  return window.claude.complete(prompt);
}

// Public entry point — returns { type: "table", rows: [...] } | { type: "error", message }
//
// Provider preference order:
//   1. /api/gemini (Cloudflare Pages Function — key stays server-side)
//   2. Direct Gemini call using env.js key (local dev / static-only hosts)
//   3. Built-in Claude helper (only available inside the Claude preview env)
async function getAiRecommendations(entry, providers = {}) {
  const prompt = buildAiPrompt(entry);
  const env = (typeof window !== "undefined" && window.DQI_ENV) || {};
  // Only honour env-supplied keys on localhost — see _isLocalhost comment.
  const geminiKey = _isLocalhost() ? (env.GEMINI_API_KEY || "").trim() : "";
  const hasProxy = !!providers.hasProxy;

  let provider = null;
  let raw = "";
  try {
    if (hasProxy) {
      provider = "gemini-2.5-flash (proxy)";
      raw = await callProxy(prompt);
    } else if (geminiKey) {
      provider = "gemini-2.5-flash";
      raw = await callGemini(geminiKey, prompt);
    } else {
      provider = "claude-haiku-4-5";
      raw = await callClaude(prompt);
    }
  } catch (e) {
    return { type: "error", message: String(e.message || e), provider };
  }

  try {
    const parsed = JSON.parse(stripFences(raw));
    const rows = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.recommendations) ? parsed.recommendations : []);
    return { type: "table", rows, provider };
  } catch (e) {
    return { type: "error", message: `Model returned non-JSON output. ${e.message}`, provider };
  }
}

// --- Recommendation card (one per failing check) ----------------------------
function RecommendationCard({ row }) {
  const dim = String(row.dimension || "—");
  const check = String(row.check || "—");
  const ctype = String(row.type || "—");
  const field = String(row.field || "—");
  const reason = String(row.reason || "");
  const recsRaw = String(row.recommendations || "");

  // Strip "1. ", "1) " prefixes so the <ol> renumbers cleanly.
  const recLines = recsRaw
    .split(/\r?\n/)
    .map((s) => s.replace(/^\s*\d+[\.\)]\s*/, "").trim())
    .filter(Boolean);

  const dimSlug = DIM_SLUG[dim] || "default";

  return (
    <div className="ai-rec-card">
      <div className="ai-rec-head">
        <span className={`ai-rec-chip dim ${dimSlug}`}>
          <span className="ai-rec-dot" aria-hidden="true"></span>
          {dim}
        </span>
        <code className="fname">{ctype}</code>
      </div>
      <div className="ai-rec-title">{check}</div>
      <div className="ai-rec-field">
        <span className="ai-rec-field-k">Field</span>
        <span className="ai-rec-field-v">{field}</span>
      </div>
      <div className="ai-rec-block">
        <div className="ai-rec-label">Why it's failing</div>
        <div className="ai-rec-text">{reason}</div>
      </div>
      <div className="ai-rec-block">
        <div className="ai-rec-label">How to fix</div>
        <ol className="ai-rec-list">
          {recLines.map((r, i) => <li key={i}>{r}</li>)}
        </ol>
      </div>
    </div>
  );
}

// --- Per-entry expander -----------------------------------------------------
function AiRecommendationEntry({ entry, cached, onGenerate, onClear, hasProvider }) {
  const [open, setOpen] = useState(false);
  const [loading, setLoading] = useState(false);

  const thr = aiThreshold();
  const failingRows = [];
  for (const dim of DIMENSIONS) {
    const checks = entry.detailed?.[dim]?.checks || [];
    for (const c of checks) {
      if (c.score < thr) {
        failingRows.push({
          dimension: DIM_LABELS[dim],
          description: c.description,
          type: c.type,
          field: c.column || "—",
          score: c.score,
        });
      }
    }
  }

  const display = entry.geo_type !== "N/A"
    ? `${entry.profile.name} — ${entry.zone_label} (${entry.geo_type})`
    : `${entry.profile.name} — ${entry.filename}`;

  const dqiVal = entry.dqi ?? 0;
  const dqiCls = dqiBandClass(dqiVal);

  async function handleGenerate() {
    setLoading(true);
    try { await onGenerate(entry); }
    finally { setLoading(false); }
  }

  return (
    <div className="score-card">
      <div className="score-card-header" onClick={() => setOpen((o) => !o)}>
        <div className="score-card-title">
          <span className={`chev ${open ? "open" : ""}`}>▶</span>
          <span>{display}</span>
        </div>
        <div className="score-card-meta">
          <span className={`dqi-pill ${dqiCls}`}>DQI {dqiVal.toFixed(1)}%</span>
        </div>
      </div>
      {open && (
        <div className="score-card-body">
          {failingRows.length > 0 ? (
            <div className="sub-section">
              <h4>Failing checks</h4>
              <table className="dqi-table">
                <thead>
                  <tr>
                    <th>Dimension</th>
                    <th>Check</th>
                    <th>Type</th>
                    <th>Field / Column</th>
                    <th className="num">Score (%)</th>
                  </tr>
                </thead>
                <tbody>
                  {failingRows.map((r, i) => (
                    <tr key={i}>
                      <td>{r.dimension}</td>
                      <td>{r.description}</td>
                      <td><code className="fname">{r.type}</code></td>
                      <td>{r.field}</td>
                      <td className="num"><HeatCell value={r.score} /></td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          ) : (
            <Banner kind="info">No failing checks for this dataset — using the Default profile.</Banner>
          )}

          <div className="footer-actions" style={{ marginTop: 12, alignItems: "center" }}>
            <button
              className="primary"
              onClick={handleGenerate}
              disabled={loading || !hasProvider}
              title={!hasProvider ? "No AI provider configured — set the GEMINI_API_KEY secret in Cloudflare Pages, or add it to env.js locally." : ""}
            >
              {loading ? "Generating…" : (cached ? "Regenerate Recommendations" : "Generate Recommendations")}
            </button>
            {cached && (
              <button className="small ghost" onClick={() => onClear(entry)} disabled={loading}>Clear</button>
            )}
            {cached?.provider && (
              <span className="hint" style={{ fontSize: 12, color: "var(--ink-3)", alignSelf: "center" }}>
                Model: <code className="fname">{cached.provider}</code>
              </span>
            )}
            {!hasProvider && !cached && (
              <span className="hint" style={{ fontSize: 12, color: "var(--ink-3)", alignSelf: "center" }}>
                Add a Gemini key in <code className="fname">env.js</code> to enable.
              </span>
            )}
          </div>

          {cached && cached.type === "error" && (
            <Banner kind="error">{cached.message}</Banner>
          )}
          {cached && cached.type === "table" && (
            cached.rows && cached.rows.length > 0 ? (
              <div className="sub-section ai-rec-grid">
                {cached.rows.map((r, i) => <RecommendationCard key={i} row={r} />)}
              </div>
            ) : (
              <Banner kind="info">Model returned no recommendations.</Banner>
            )
          )}
        </div>
      )}
    </div>
  );
}

// --- Top-level section ------------------------------------------------------
function AiRecommendationsSection({ parsed, cache, setCache }) {
  const env = (typeof window !== "undefined" && window.DQI_ENV) || {};
  // env-supplied Gemini key is only usable on localhost; on deployed hosts
  // the proxy is the only viable path (the key would be exposed otherwise).
  const hasGemini = _isLocalhost() && !!(env.GEMINI_API_KEY || "").trim();
  const envKeyButNotLocal = !_isLocalhost() && !!(env.GEMINI_API_KEY || "").trim();
  const hasClaude = !!(typeof window !== "undefined" && window.claude && typeof window.claude.complete === "function");

  // Probe /api/gemini once on mount. If the Cloudflare Pages Function is
  // wired up and the GEMINI_API_KEY secret is set, this returns { available: true }
  // and the proxy becomes the preferred provider — key stays server-side.
  // `null` = probing, `false` = endpoint missing or secret not set.
  const [hasProxy, setHasProxy] = React.useState(null);
  React.useEffect(() => {
    let alive = true;
    probeProxy().then((info) => {
      if (alive) setHasProxy(!!info?.available);
    });
    return () => { alive = false; };
  }, []);

  // Build the list of entries that warrant a recommendation: Default profile
  // OR any entry with at least one failing check. Mirrors Streamlit.
  const lowScoreEntries = [];
  for (const entries of Object.values(parsed)) {
    for (const e of entries) {
      if (e.dqi == null) continue;
      if (e.profile.filename === "__default__" || hasFailingCheck(e)) {
        lowScoreEntries.push(e);
      }
    }
  }

  if (lowScoreEntries.length === 0) return null;

  const thr = aiThreshold();
  const probing = hasProxy === null;
  const hasProvider = !!hasProxy || hasGemini || hasClaude;

  async function handleGenerate(entry) {
    const key = _aiCacheKey(entry);
    const result = await getAiRecommendations(entry, { hasProxy: !!hasProxy });
    setCache((c) => ({ ...c, [key]: result }));
  }
  function handleClear(entry) {
    const key = _aiCacheKey(entry);
    setCache((c) => { const n = { ...c }; delete n[key]; return n; });
  }

  return (
    <>
      <hr className="divider" />
      <div className="card">
        <h3 className="section-title">AI Recommendations</h3>
        <p className="section-caption">
          The following datasets have failing checks (below {thr.toFixed(0)}%) or use the Default profile.
          Click a dataset to generate profile-specific improvement recommendations.
        </p>

        {hasProxy && (
          <Banner kind="info">
            Using server-side proxy (<code className="fname">/api/gemini</code>) — your Gemini key is stored as a Cloudflare secret and never reaches the browser.
          </Banner>
        )}
        {!hasProxy && !probing && !hasGemini && !hasClaude && (
          <Banner kind="warn">
            <strong>AI Recommendations disabled.</strong> {envKeyButNotLocal ? (
              <>A Gemini key in <code className="fname">env.js</code> is ignored on deployed hosts because it would be exposed to every visitor. Set the <code className="fname">GEMINI_API_KEY</code> secret in your Cloudflare Pages project instead — see <code className="fname">SETUP.md</code>.</>
            ) : (
              <>Either set the <code className="fname">GEMINI_API_KEY</code> secret in your Cloudflare Pages project (recommended — see README), or add a key to <code className="fname">env.js</code> locally. Free keys: <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer">aistudio.google.com/apikey</a>.</>
            )}
          </Banner>
        )}
        {!hasProxy && !probing && !hasGemini && hasClaude && (
          <Banner kind="info">
            Using the built-in Claude helper (only available inside the Claude preview environment — won't work on a deployed site). For production, set the <code className="fname">GEMINI_API_KEY</code> secret in Cloudflare Pages.
          </Banner>
        )}

        {lowScoreEntries.map((entry) => {
          const key = _aiCacheKey(entry);
          return (
            <AiRecommendationEntry
              key={key}
              entry={entry}
              cached={cache[key]}
              onGenerate={handleGenerate}
              onClear={handleClear}
              hasProvider={hasProvider}
            />
          );
        })}
      </div>
    </>
  );
}

Object.assign(window, {
  // AI_THRESHOLD is set by dqi_engine.js — don't re-assign here (last-write
  // wins, easy to drift if the engine ever changes the source).
  AiRecommendationsSection, AiRecommendationEntry, RecommendationCard,
  getAiRecommendations, hasFailingCheck,
});
