// GA4 Data API over plain REST — no client libraries.
// Auth: service-account JWT (RS256 via node:crypto) exchanged for an access
// token at Google's token endpoint, cached until expiry.

import fs from 'node:fs';
import crypto from 'node:crypto';
import { currentMarkets, currentGaFilters } from './market.js';

let creds = null;
function loadCreds() {
  if (creds) return creds;
  const p = process.env.GOOGLE_APPLICATION_CREDENTIALS;
  if (!p || !fs.existsSync(p)) {
    throw new Error(
      'Google credentials not found. Set GOOGLE_APPLICATION_CREDENTIALS in .env to the path of your service-account JSON key (see SETUP-GA4.md), or run with DEMO_MODE=1.'
    );
  }
  creds = JSON.parse(fs.readFileSync(p, 'utf8'));
  return creds;
}

const b64url = (buf) => Buffer.from(buf).toString('base64url');

// Network-resilient fetch: Google being briefly unreachable (Wi-Fi blip, VPN
// handshake, DNS hiccup) must NOT surface as a broken dashboard. Retry twice
// with backoff, then fail with a message a human can act on.
const NET_ERR = /fetch failed|UND_ERR|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket|network/i;
async function fetchRetry(url, opts, tries = 3) {
  let last;
  for (let i = 0; i < tries; i++) {
    try {
      return await fetch(url, opts);
    } catch (e) {
      last = e;
      const msg = String(e?.cause?.code || e?.cause?.message || e?.message || e);
      if (!NET_ERR.test(msg)) throw e; // a real error, not a connectivity one
      if (i < tries - 1) await new Promise((r) => setTimeout(r, 800 * (i + 1)));
    }
  }
  const err = new Error('Google Analytics is unreachable right now — the server could not connect to googleapis.com (network timeout). Check the internet connection or VPN on the machine running the app; recent cached data is shown where available.');
  err.cause = last;
  err.gaUnreachable = true;
  throw err;
}

let tokenCache = { token: null, exp: 0 };

export async function accessToken() {
  if (tokenCache.token && Date.now() < tokenCache.exp - 60000) return tokenCache.token;
  const c = loadCreds();
  const now = Math.floor(Date.now() / 1000);
  const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
  const claims = b64url(
    JSON.stringify({
      iss: c.client_email,
      scope: 'https://www.googleapis.com/auth/analytics.readonly',
      aud: 'https://oauth2.googleapis.com/token',
      iat: now,
      exp: now + 3600,
    })
  );
  const unsigned = `${header}.${claims}`;
  const signature = crypto.createSign('RSA-SHA256').update(unsigned).sign(c.private_key);
  const jwt = `${unsigned}.${b64url(signature)}`;

  const res = await fetchRetry('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      assertion: jwt,
    }),
  });
  if (!res.ok) throw new Error(`Google auth failed (${res.status}): ${await res.text()}`);
  const data = await res.json();
  tokenCache = { token: data.access_token, exp: Date.now() + (data.expires_in || 3600) * 1000 };
  return tokenCache.token;
}

// Thin wrapper over properties/{id}:runReport — with a 10-minute cache so
// dashboards feel instant after first load and GA4 quota stays low.
const rrCache = new Map();
const RR_TTL = 10 * 60 * 1000;
// When the request carries an analytics market selection, EVERY report gets
// a `country IN (…)` filter — one place, all graphs. (The 10-min cache key is
// the full body, so filtered and unfiltered results never mix.)
function withMarketFilter(body) {
  // Locale-mode selections carry per-market specs (country and/or browser-
  // language prefix, OR-combined) — this is what separates two markets that
  // share a country, like US English vs US Spanish. Country-mode selections
  // fall back to the plain country IN (…) filter.
  const specs = currentGaFilters();
  let marketExpr = null;
  if (specs) {
    const exprs = specs.map((sp) => {
      const parts = [];
      if (sp.country) parts.push({ filter: { fieldName: 'country', inListFilter: { values: [sp.country], caseSensitive: false } } });
      if (sp.langPrefix) parts.push({ filter: { fieldName: 'languageCode', stringFilter: { matchType: 'BEGINS_WITH', value: sp.langPrefix, caseSensitive: false } } });
      if (!parts.length) return null;
      return parts.length === 1 ? parts[0] : { andGroup: { expressions: parts } };
    }).filter(Boolean);
    if (exprs.length) marketExpr = exprs.length === 1 ? exprs[0] : { orGroup: { expressions: exprs } };
  }
  if (!marketExpr) {
    const countries = currentMarkets();
    if (!countries) return body;
    marketExpr = { filter: { fieldName: 'country', inListFilter: { values: countries, caseSensitive: false } } };
  }
  return {
    ...body,
    dimensionFilter: body.dimensionFilter
      ? { andGroup: { expressions: [body.dimensionFilter, marketExpr] } }
      : marketExpr,
  };
}

const rrInflight = new Map(); // identical concurrent GA queries share one call
const RR_STALE = 60 * 60 * 1000;
export async function runReport(propertyId, body0) {
  const body = withMarketFilter(body0);
  const key = propertyId + '|' + JSON.stringify(body);
  const hit = rrCache.get(key);
  if (hit && Date.now() - hit.t < RR_TTL) return hit.v;
  if (rrInflight.has(key)) {
    if (hit && Date.now() - hit.t < RR_STALE) return hit.v;
    return rrInflight.get(key);
  }
  if (rrCache.size > 500) rrCache.clear();
  const run = (async () => {
    try {
      const v = await runReportLive(propertyId, body);
      rrCache.set(key, { t: Date.now(), v });
      return v;
    } catch (err) {
      // Last-known-good beats a hole in the dashboard: if we EVER answered
      // this exact query, serve that result while Google is unreachable.
      const prev = rrCache.get(key);
      if (prev && err?.gaUnreachable) return prev.v;
      throw err;
    }
  })();
  const stored = run.finally(() => rrInflight.delete(key));
  // Mark handled — a failing background refresh after a stale-serve must
  // never become a process-killing unhandled rejection (see db.js).
  stored.catch(() => {});
  rrInflight.set(key, stored);
  if (hit && Date.now() - hit.t < RR_STALE) return hit.v;
  return stored;
}
async function runReportLive(propertyId, body) {
  const token = await accessToken();
  const res = await fetchRetry(
    `https://analyticsdata.googleapis.com/v1beta/properties/${propertyId}:runReport`,
    {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    }
  );
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`GA4 API error for property ${propertyId} (${res.status}): ${text.slice(0, 500)}`);
  }
  return res.json();
}
