// Rule-based insights engine — turns raw GA4 numbers into facts + PR-flavoured
// suggestions. Pure computation, no AI needed (the AI summary builds on top).
//
// Each insight: { kind: 'up'|'down'|'watch'|'info', fact, suggestion? }

import {
  loadSites, totals, timeseries, breakdown,
  portfolioTotals, resolveRange, previousRange,
} from './ga4.js';
import { emailOverview } from './email.js';
import { legacyOverview } from './legacy.js';
import { releaseTracker } from './releases.js';

const pctOf = (cur, prev) => (prev ? (cur - prev) / prev : null);
const fmtPct = (d) => `${d > 0 ? '+' : ''}${(d * 100).toFixed(1)}%`;
const fmtInt = (n) => Math.round(n).toLocaleString('en-GB');
const median = (arr) => {
  const s = [...arr].sort((a, b) => a - b);
  return s.length ? s[Math.floor(s.length / 2)] : 0;
};

function sessionsMoveInsight(cur, prv, label) {
  const d = pctOf(cur.sessions, prv.sessions);
  if (d == null || Math.abs(d) < 0.03) {
    return { kind: 'info', fact: `${label} traffic is steady: ${fmtInt(cur.sessions)} sessions (${d == null ? 'no previous period' : fmtPct(d)} vs previous period).` };
  }
  if (d > 0) {
    return {
      kind: 'up',
      fact: `${label} traffic is up ${fmtPct(d)} — ${fmtInt(cur.sessions)} sessions vs ${fmtInt(prv.sessions)} in the previous period.`,
      suggestion: 'Capture what worked this period (releases, coverage, campaigns) and repeat the cadence next period.',
    };
  }
  return {
    kind: 'down',
    fact: `${label} traffic is down ${fmtPct(d)} — ${fmtInt(cur.sessions)} sessions vs ${fmtInt(prv.sessions)} previously.`,
    suggestion: 'Check publishing cadence and distribution: fewer releases, weaker headlines, or a drop in a key channel usually explains it.',
  };
}

async function siteInsights(siteId, range, prev) {
  const [cur, prv, ts, pagesCur, pagesPrev, chCur, chPrev] = await Promise.all([
    totals(siteId, range.startDate, range.endDate),
    totals(siteId, prev.startDate, prev.endDate),
    timeseries(siteId, range.startDate, range.endDate),
    breakdown(siteId, 'pagePath', range.startDate, range.endDate, 30),
    breakdown(siteId, 'pagePath', prev.startDate, prev.endDate, 30),
    breakdown(siteId, 'sessionDefaultChannelGroup', range.startDate, range.endDate, 10),
    breakdown(siteId, 'sessionDefaultChannelGroup', prev.startDate, prev.endDate, 10),
  ]);
  const out = [];

  // 1. Overall movement
  out.push(sessionsMoveInsight(cur, prv, 'Site'));

  // 2. Spike day
  const sess = ts.map((r) => r.sessions);
  const med = median(sess);
  const peak = ts.reduce((a, b) => (b.sessions > (a?.sessions ?? -1) ? b : a), null);
  if (peak && med > 0 && peak.sessions > med * 1.8) {
    out.push({
      kind: 'up',
      fact: `Traffic spiked on ${peak.date}: ${fmtInt(peak.sessions)} sessions — ${(peak.sessions / med).toFixed(1)}× the typical day.`,
      suggestion: 'Identify what drove that day (release, coverage pickup, social post) — that is your highest-leverage playbook to repeat.',
    });
  }

  // 3. Quiet stretch
  if (med > 0) {
    const quiet = sess.filter((v) => v < med * 0.5).length;
    if (quiet >= Math.max(3, Math.floor(sess.length * 0.15))) {
      out.push({
        kind: 'watch',
        fact: `${quiet} day${quiet > 1 ? 's' : ''} ran below half the typical traffic level this period.`,
        suggestion: 'Long quiet gaps between releases flatten momentum — consider a steadier drumbeat of smaller stories.',
      });
    }
  }

  // 4. Page movers (matched by path)
  const prevByPath = Object.fromEntries(pagesPrev.map((p) => [p.dimensionValue, p.screenPageViews || 0]));
  const movers = pagesCur
    .map((p) => ({ path: p.dimensionValue, cur: p.screenPageViews || 0, prev: prevByPath[p.dimensionValue] ?? 0 }))
    .map((m) => ({ ...m, diff: m.cur - m.prev }));
  const riser = movers.filter((m) => m.prev > 0).sort((a, b) => b.diff - a.diff)[0];
  if (riser && riser.diff > Math.max(50, riser.prev * 0.3)) {
    out.push({
      kind: 'up',
      fact: `"${riser.path}" is the biggest climber: ${fmtInt(riser.cur)} views (${fmtPct(pctOf(riser.cur, riser.prev))} vs previous period).`,
      suggestion: 'This content has momentum — push it again on social/email and link related releases to it.',
    });
  }
  const faller = movers.filter((m) => m.prev > 100).sort((a, b) => a.diff - b.diff)[0];
  if (faller && faller.diff < -faller.prev * 0.4) {
    out.push({
      kind: 'down',
      fact: `"${faller.path}" fell hardest: ${fmtInt(faller.cur)} views (${fmtPct(pctOf(faller.cur, faller.prev))}).`,
      suggestion: 'If this page still matters, refresh it or re-link it from new releases; if it was a one-off story, this decay is normal.',
    });
  }
  const fresh = movers.filter((m) => m.prev === 0 && m.cur > 100).sort((a, b) => b.cur - a.cur)[0];
  if (fresh) {
    out.push({
      kind: 'up',
      fact: `Best new content this period: "${fresh.path}" with ${fmtInt(fresh.cur)} views from a standing start.`,
      suggestion: 'New pages earning this fast deserve a second push — syndicate, pitch to journalists who missed it, add imagery to the gallery.',
    });
  }

  // 5. Concentration risk
  const totalViews = pagesCur.reduce((a, p) => a + (p.screenPageViews || 0), 0);
  const topPage = pagesCur[0];
  if (topPage && totalViews > 0) {
    const share = (topPage.screenPageViews || 0) / totalViews;
    if (share > 0.45 && topPage.dimensionValue !== '/') {
      out.push({
        kind: 'watch',
        fact: `"${topPage.dimensionValue}" carries ${(share * 100).toFixed(0)}% of all page views — traffic is heavily concentrated on one page.`,
        suggestion: 'Use that page as a hub: add prominent links from it to recent releases and galleries to spread the audience.',
      });
    }
  }

  // 6. Channel movers
  const chPrevBy = Object.fromEntries(chPrev.map((c) => [c.dimensionValue, c.sessions || 0]));
  const chMovers = chCur
    .map((c) => ({ ch: c.dimensionValue, cur: c.sessions || 0, prev: chPrevBy[c.dimensionValue] ?? 0 }))
    .filter((c) => c.prev > 30 || c.cur > 30)
    .map((c) => ({ ...c, d: pctOf(c.cur, c.prev) }));
  const chUp = chMovers.filter((c) => c.d != null && c.d > 0.15).sort((a, b) => b.d - a.d)[0];
  if (chUp) {
    out.push({
      kind: 'up',
      fact: `${chUp.ch} is growing: ${fmtInt(chUp.cur)} sessions, ${fmtPct(chUp.d)} vs previous period.`,
      suggestion: chUp.ch.includes('Social')
        ? 'Social is responding — increase posting frequency around releases while the algorithm is favouring you.'
        : `Double down on what feeds ${chUp.ch} while it has momentum.`,
    });
  }
  const chDown = chMovers.filter((c) => c.d != null && c.d < -0.15).sort((a, b) => a.d - b.d)[0];
  if (chDown) {
    out.push({
      kind: 'down',
      fact: `${chDown.ch} is fading: ${fmtInt(chDown.cur)} sessions, ${fmtPct(chDown.d)} vs previous period.`,
      suggestion: chDown.ch === 'Organic Search'
        ? 'Falling organic search often means older releases dropping off — check headline keywords and internal linking on recent posts.'
        : chDown.ch === 'Direct'
          ? 'Direct dips usually track brand attention — worth checking whether press-office links in outgoing material are current.'
          : `Review what previously drove ${chDown.ch} and whether that activity stopped.`,
    });
  }

  // 7. Engagement
  const engD = pctOf(cur.engagementRate, prv.engagementRate);
  if (engD != null && Math.abs(engD) > 0.08) {
    out.push(
      engD > 0
        ? { kind: 'up', fact: `Visitors are more engaged: engagement rate ${(cur.engagementRate * 100).toFixed(1)}% (${fmtPct(engD)}).` }
        : {
            kind: 'watch',
            fact: `Engagement rate slipped to ${(cur.engagementRate * 100).toFixed(1)}% (${fmtPct(engD)}).`,
            suggestion: 'More traffic bouncing quickly can mean clickbait-y distribution or slow pages — check the top landing pages.',
          }
    );
  }

  // 8. New-audience share
  if (cur.totalUsers > 0) {
    const newShare = cur.newUsers / cur.totalUsers;
    if (newShare > 0.7) {
      out.push({
        kind: 'info',
        fact: `${(newShare * 100).toFixed(0)}% of this period's audience were first-time visitors.`,
        suggestion: 'Strong reach, weak retention — a newsletter sign-up or follow prompt on release pages would compound this audience.',
      });
    } else if (newShare < 0.3) {
      out.push({
        kind: 'info',
        fact: `Audience is mostly returning visitors (${(100 - newShare * 100).toFixed(0)}%).`,
        suggestion: 'A loyal core (likely journalists) keeps coming back — worth serving them with an embargo area or media-alerts list.',
      });
    }
  }

  // 9. Email campaigns (when this site sends them)
  try {
    const email = await emailOverview(siteId, `${range.startDate}..${range.endDate}`);
    if (email.enabled && email.campaigns?.length) {
      const cs = email.campaigns;
      const avgOpen = cs.reduce((a, c) => a + c.openRate, 0) / cs.length;
      const best = [...cs].sort((a, b) => b.openRate - a.openRate)[0];
      if (best && best.openRate > avgOpen * 1.25 && cs.length >= 2) {
        out.push({
          kind: 'up',
          fact: `Best email this period: "${best.name}" — ${(best.openRate * 100).toFixed(1)}% open rate vs a ${(avgOpen * 100).toFixed(1)}% average.`,
          suggestion: 'Study its subject line and send time — that formula is beating your baseline.',
        });
      }
      // Open-rate slide across recent sends (needs dated campaigns)
      const dated = cs.filter((c) => c.date).sort((a, b) => a.date.localeCompare(b.date));
      if (dated.length >= 3) {
        const lastThree = dated.slice(-3);
        if (lastThree.every((c, i) => i === 0 || c.openRate < lastThree[i - 1].openRate)) {
          out.push({
            kind: 'watch',
            fact: `Email open rates have fallen three sends in a row (${lastThree.map((c) => (c.openRate * 100).toFixed(0) + '%').join(' → ')}).`,
            suggestion: 'List fatigue or subject-line drift — try a re-engagement segment or fresh subject styles.',
          });
        }
      }
      // Did send days move site traffic?
      const byDate = Object.fromEntries(ts.map((r) => [r.date, r.sessions]));
      const med = median(ts.map((r) => r.sessions));
      const lifts = email.sendDays
        .map((d) => ({ ...d, sessions: byDate[d.date] }))
        .filter((d) => d.sessions != null && med > 0)
        .map((d) => ({ ...d, lift: d.sessions / med }));
      const bigLift = lifts.filter((l) => l.lift >= 1.35).sort((a, b) => b.lift - a.lift)[0];
      if (bigLift) {
        out.push({
          kind: 'up',
          fact: `Email is moving the needle: on send day ${bigLift.date} site traffic ran ${bigLift.lift.toFixed(1)}× the typical day.`,
          suggestion: 'Campaigns are a reliable traffic lever here — consider a steadier send cadence.',
        });
      } else if (lifts.length >= 2 && lifts.every((l) => l.lift < 1.1)) {
        out.push({
          kind: 'watch',
          fact: `Email sends aren't visibly lifting site traffic (${lifts.length} send days all within 10% of a typical day).`,
          suggestion: 'Check the campaigns link prominently to the site, and that links carry UTM tags so GA4 credits the Email channel.',
        });
      }
    }
  } catch { /* email layer is optional — never break insights */ }

  // 9b. Content volume vs audience — is the traffic move content-led?
  try {
    const lg = await legacyOverview(siteId, `${range.startDate}..${range.endDate}`);
    if (lg.enabled && lg.cadence && lg.cadence.prevTotal > 0) {
      const cd = (lg.cadence.total - lg.cadence.prevTotal) / lg.cadence.prevTotal;
      const sd = prv.sessions ? (cur.sessions - prv.sessions) / prv.sessions : null;
      const perRel = lg.cadence.total ? Math.round(cur.sessions / lg.cadence.total) : null;
      if (sd != null && Math.abs(cd) >= 0.15 && Math.sign(cd) === Math.sign(sd)) {
        out.push({
          kind: 'info',
          fact: `Traffic moved WITH publishing volume: ${lg.cadence.total} releases (${fmtPct(cd)}) vs sessions ${fmtPct(sd)} — this looks content-led${perRel ? ` (~${perRel.toLocaleString('en-GB')} sessions per release)` : ''}.`,
          suggestion: 'Before diagnosing channels or SEO, check the publishing calendar — volume explains most of this move.',
        });
      } else if (sd != null && Math.abs(cd) < 0.1 && Math.abs(sd) >= 0.1) {
        out.push({
          kind: sd > 0 ? 'up' : 'watch',
          fact: `Publishing volume was steady (${lg.cadence.total} releases, ${fmtPct(cd)}) yet sessions moved ${fmtPct(sd)} — NOT content-led; distribution or interest changed.`,
          suggestion: sd > 0 ? 'Same output, more audience — find which channel or story over-delivered and lean in.' : 'Same output, less audience — look at channels, email cadence and story selection rather than volume.',
        });
      } else if (sd != null && Math.sign(cd) !== Math.sign(sd) && Math.abs(cd) >= 0.15) {
        out.push({
          kind: cd < 0 ? 'up' : 'watch',
          fact: `Publishing and audience diverged: releases ${fmtPct(cd)} but sessions ${fmtPct(sd)} — efficiency per release ${cd < 0 ? 'rose sharply' : 'fell'}.`,
          suggestion: cd < 0 ? 'Fewer, stronger stories are out-performing volume — quality is winning.' : 'More output is not translating into audience — review story selection and distribution.',
        });
      }
    }
  } catch { /* optional */ }

  // 10. Do emailed releases outperform non-emailed ones?
  try {
    const rt = await releaseTracker(siteId, '90d');
    const emailed = rt.releases.filter((r) => r.email && !r.preExisting);
    const plain = rt.releases.filter((r) => !r.email && !r.preExisting);
    if (emailed.length >= 2 && plain.length >= 2) {
      const mean = (arr) => arr.reduce((a, r) => a + r.first7, 0) / arr.length;
      const ratio = mean(emailed) / Math.max(1, mean(plain));
      if (ratio >= 1.3) {
        out.push({
          kind: 'up',
          fact: `Emailed releases earn ${ratio.toFixed(1)}× the first-week views of non-emailed ones (${emailed.length} emailed vs ${plain.length} not, last 90 days).`,
          suggestion: 'Make an email send part of the standard release checklist — the data says it pays for itself.',
        });
      } else if (ratio <= 0.8) {
        out.push({
          kind: 'watch',
          fact: `Emailed releases are underperforming non-emailed ones (${ratio.toFixed(1)}× first-week views).`,
          suggestion: 'Check send timing versus publish time, and that campaign links land on the release page with UTM tags.',
        });
      }
    }
  } catch { /* optional */ }

  return out;
}

async function portfolioInsights(range, prev, siteList = null) {
  const [cur, prv] = await Promise.all([
    portfolioTotals(range.startDate, range.endDate, siteList),
    portfolioTotals(prev.startDate, prev.endDate, siteList),
  ]);
  const out = [];
  out.push(sessionsMoveInsight(cur.totals, prv.totals, 'Portfolio'));

  const prevById = Object.fromEntries(prv.perSite.map((s) => [s.id, s]));
  const movers = cur.perSite
    .map((s) => ({ name: s.name, cur: s.sessions, prev: prevById[s.id]?.sessions ?? 0 }))
    .filter((s) => s.prev > 50)
    .map((s) => ({ ...s, d: pctOf(s.cur, s.prev) }));

  const up = movers.filter((s) => s.d > 0.1).sort((a, b) => b.d - a.d)[0];
  if (up) {
    out.push({
      kind: 'up',
      fact: `${up.name} is the fastest-growing site: ${fmtInt(up.cur)} sessions, ${fmtPct(up.d)} vs previous period.`,
      suggestion: 'Look at what this site did differently this period — cadence, content type, channel mix — and template it for the others.',
    });
  }
  const down = movers.filter((s) => s.d < -0.1).sort((a, b) => a.d - b.d)[0];
  if (down) {
    out.push({
      kind: 'down',
      fact: `${down.name} declined most: ${fmtInt(down.cur)} sessions, ${fmtPct(down.d)}.`,
      suggestion: 'Open its site page and check the channel breakdown — a single fading channel usually explains a site-level dip.',
    });
  }

  // Share concentration across portfolio
  const totalSess = cur.perSite.reduce((a, s) => a + s.sessions, 0);
  const top = [...cur.perSite].sort((a, b) => b.sessions - a.sessions)[0];
  if (top && totalSess > 0 && top.sessions / totalSess > 0.5 && cur.perSite.length > 3) {
    out.push({
      kind: 'watch',
      fact: `${top.name} generates ${((top.sessions / totalSess) * 100).toFixed(0)}% of all portfolio traffic.`,
      suggestion: 'Healthy for that client, risky for the portfolio — apply its playbook to the mid-tier sites to balance the book.',
    });
  }

  // Conversions movement
  const convD = pctOf(cur.totals.conversions, prv.totals.conversions);
  if (convD != null && Math.abs(convD) > 0.1) {
    out.push(
      convD > 0
        ? { kind: 'up', fact: `Portfolio conversions up ${fmtPct(convD)} (${fmtInt(cur.totals.conversions)} total).` }
        : { kind: 'down', fact: `Portfolio conversions down ${fmtPct(convD)} (${fmtInt(cur.totals.conversions)} total).`, suggestion: 'Check which site drove the drop in the table below — conversion dips are usually one site, not all.' }
    );
  }

  return out;
}

export async function computeInsights(scope, rangeStr, allowedSites = null) {
  const range = resolveRange(rangeStr);
  const prev = previousRange(range);
  const insights = scope === 'all' ? await portfolioInsights(range, prev, allowedSites) : await siteInsights(scope, range, prev);
  // Release paths in the facts ("/releases/…/121512") mean nothing to a
  // human — append the real release TITLE from the reports DB.
  if (scope !== 'all') {
    try {
      const { releaseTitles } = await import('./legacy.js');
      const idOf = (s) => /\/(\d{3,})\/?"/.exec(s)?.[1] || null;
      const ids = [...new Set(insights.map((i) => idOf(i.fact)).filter(Boolean))];
      if (ids.length) {
        const titles = await releaseTitles(scope, ids.slice(0, 20)).catch(() => ({}));
        for (const i of insights) {
          const id = idOf(i.fact);
          const t = id && titles[id];
          const title = t && (typeof t === 'string' ? t : t.title);
          if (title) i.fact = i.fact.replace(/(".*?\/(\d{3,})\/?")/, `$1 — “${String(title).slice(0, 90)}”`);
        }
      }
    } catch { /* facts still work without titles */ }
  }
  // Order: wins first, then risks, then info — reads like a briefing.
  const order = { up: 0, down: 1, watch: 2, info: 3 };
  insights.sort((a, b) => order[a.kind] - order[b.kind]);
  return { range, insights };
}
