import type {DashboardBriefing, DashboardTask, Insight} from '@/features/dashboard/types/dashboard';
import type {RangeKey} from '@/shared/types/api';
import {buildTimeseries} from './dashboard.mock';
import {buildStoreComparison, buildRewardUsage} from './analytics.mock';
import {buildStaffSummary} from '@/features/staff/mock/staff.mock';
import {buildRewards} from '@/features/lyts/mock/lyts.mock';
import {storeName} from '@/features/stores/mock/stores.mock';

/**
 * The narrative half of the dashboard.
 *
 * Every sentence here is DERIVED from the same generators the charts read —
 * buildTimeseries, buildStoreComparison, buildRewardUsage, buildStaffSummary,
 * buildRewards. Nothing is invented. That is the whole point: a summary that
 * says footfall rose while the footfall chart above it falls is worse than no
 * summary at all, and independent fixtures are how that happens. When these
 * become a real model call, the model gets fed the same aggregates.
 *
 * It also means the copy moves when the scope does. Switch to one store and
 * the summary talks about that store; switch the range and the percentages
 * follow, because they are recomputed from the series rather than templated.
 */

const RUPEES = new Intl.NumberFormat('en-IN', {
  style: 'currency',
  currency: 'INR',
  maximumFractionDigits: 0,
  notation: 'compact',
});
const COUNT = new Intl.NumberFormat('en-IN', {
  notation: 'compact',
  maximumFractionDigits: 1,
});

const RANGE_WORD: Record<RangeKey, string> = {
  '7d': 'the last 7 days',
  '30d': 'the last 30 days',
  '90d': 'the last 90 days',
  mtd: 'the month so far',
  ytd: 'the year so far',
  // The generators receive a RangeKey, not the custom window's endpoints, so
  // the narrative cannot name the dates yet. Reads correctly either way; give
  // buildBriefing the start/end once the query carries them.
  custom: 'the selected period',
};

/** Totals for a window, plus how it moved against the window before it. */
function windowTotals(range: RangeKey, storeId: string, endMs: number) {
  const points = buildTimeseries(range, storeId, endMs);
  const half = Math.floor(points.length / 2);
  const sum = (xs: typeof points, k: 'visitors' | 'purchases' | 'revenue') =>
    xs.reduce((a, p) => a + p[k], 0);

  const recent = points.slice(half);
  const prior = points.slice(0, half);

  const visitors = sum(points, 'visitors');
  const purchases = sum(points, 'purchases');
  const revenue = sum(points, 'revenue');

  // Compared half-to-half rather than against a separately generated previous
  // period: the two halves come from one series, so the direction stated in
  // prose is guaranteed to be the direction drawn on the chart.
  const priorRevenue = sum(prior, 'revenue');
  const revenueDeltaPct = priorRevenue
    ? ((sum(recent, 'revenue') - priorRevenue) / priorRevenue) * 100
    : 0;

  return {
    visitors,
    purchases,
    revenue,
    conversionPct: visitors ? (purchases / visitors) * 100 : 0,
    revenueDeltaPct,
  };
}

function buildAlerts(
  storeId: string,
  range: RangeKey,
  endMs: number,
): Insight[] {
  const alerts: Insight[] = [];

  // 1. Rewards about to expire — the only thing on this dashboard with a
  //    hard deadline, so it outranks everything else.
  const rewards = buildRewards(storeId, range, endMs);
  const expiring = rewards.filter((r) => {
    if (!r.expiresAt) return false;
    const days = (Date.parse(r.expiresAt) - endMs) / 86400000;
    return days > 0 && days <= 3;
  });
  if (expiring.length > 0) {
    alerts.push({
      id: 'alert-expiring',
      severity: 'error',
      title: `${expiring.length} reward${expiring.length > 1 ? 's' : ''} expiring within 72 hours`,
      body: expiring.map((r) => r.name).join(', ') + '.',
      action: {label: 'Review rewards', href: '/lyts'},
    });
  }

  // 2. Staff absence, measured against the roster rather than a fixed count —
  //    two absent out of four is a different day from two out of twenty.
  const staff = buildStaffSummary(storeId, range);
  const missing = staff.absent + staff.onLeave;
  if (staff.total > 0 && missing / staff.total >= 0.2) {
    alerts.push({
      id: 'alert-staffing',
      severity: 'warning',
      title: `${missing} of ${staff.total} staff are not on the floor`,
      body: `${staff.absent} absent, ${staff.onLeave} on leave, ${staff.late} late. Cover may be short at peak.`,
      action: {label: 'Open staff', href: '/staff'},
    });
  }

  // 3. The weakest store, but only when there is a spread worth acting on.
  if (storeId === 'all') {
    const stores = buildStoreComparison(range, endMs);
    if (stores.length > 1) {
      const sorted = [...stores].sort((a, b) => a.revenueInr - b.revenueInr);
      const worst = sorted[0];
      const best = sorted[sorted.length - 1];
      const gapPct = best.revenueInr
        ? ((best.revenueInr - worst.revenueInr) / best.revenueInr) * 100
        : 0;
      if (gapPct >= 25) {
        alerts.push({
          id: 'alert-store-gap',
          severity: 'warning',
          title: `${worst.name} is ${Math.round(gapPct)}% behind ${best.name}`,
          body: `${RUPEES.format(worst.revenueInr)} against ${RUPEES.format(best.revenueInr)} over ${RANGE_WORD[range]}.`,
          action: {label: 'Compare stores', href: '/stores'},
        });
      }
    }
  }

  // 4. A reward people claim and never redeem is unspent liability sitting on
  //    the books — worth surfacing even though nothing is technically broken.
  const usage = buildRewardUsage(storeId, range);
  const dead = usage
    .filter((r) => r.claimed >= 100 && r.usageRatePct < 30)
    .sort((a, b) => a.usageRatePct - b.usageRatePct)[0];
  if (dead) {
    alerts.push({
      id: 'alert-dead-reward',
      severity: 'info',
      title: `${dead.name} is claimed but not redeemed`,
      body: `${COUNT.format(dead.claimed)} claims, ${Math.round(dead.usageRatePct)}% redeemed. The rest is outstanding liability.`,
      action: {label: 'Open LYTs', href: '/lyts'},
    });
  }

  return alerts;
}

function buildTasks(
  storeId: string,
  range: RangeKey,
  endMs: number,
): DashboardTask[] {
  const staff = buildStaffSummary(storeId, range);
  const rewards = buildRewards(storeId, range, endMs);
  const expiring = rewards.filter((r) => {
    if (!r.expiresAt) return false;
    const days = (Date.parse(r.expiresAt) - endMs) / 86400000;
    return days > 0 && days <= 3;
  });

  const tasks: DashboardTask[] = [
    {
      id: 'task-roster',
      label: 'Confirm the evening roster',
      detail:
        staff.late > 0
          ? `${staff.late} arrived late today`
          : 'Peak footfall starts at 18:30',
      due: 'before 17:00',
      isDone: false,
      action: {label: 'Staff', href: '/staff'},
    },
    {
      id: 'task-float',
      label: 'Reconcile the till float',
      due: 'end of day',
      isDone: false,
    },
  ];

  if (expiring.length > 0) {
    tasks.unshift({
      id: 'task-expiring',
      label: `Decide on ${expiring.length} expiring reward${expiring.length > 1 ? 's' : ''}`,
      detail: 'Extend, or let them lapse and clear the liability',
      due: 'today',
      isDone: false,
      action: {label: 'Review', href: '/lyts'},
    });
  }

  // Deterministic and already ticked, so the list reads as a real day in
  // progress rather than an empty form.
  tasks.push({
    id: 'task-open',
    label: 'Open-of-day checks',
    detail: storeId === 'all' ? 'All stores reported' : storeName(storeId),
    due: 'morning',
    isDone: true,
  });

  return tasks;
}

export function buildBriefing(
  storeId: string,
  range: RangeKey,
  endMs: number,
): DashboardBriefing {
  const t = windowTotals(range, storeId, endMs);
  const scope = storeId === 'all' ? 'across all stores' : `at ${storeName(storeId)}`;
  const dir = t.revenueDeltaPct >= 0 ? 'up' : 'down';
  const magnitude = Math.abs(Math.round(t.revenueDeltaPct));

  const stores = storeId === 'all' ? buildStoreComparison(range, endMs) : [];
  const leader = stores.length
    ? [...stores].sort((a, b) => b.revenueInr - a.revenueInr)[0]
    : null;

  const sentences = [
    `Over ${RANGE_WORD[range]} ${scope}, ${COUNT.format(t.visitors)} visits turned into ${COUNT.format(t.purchases)} purchases — a ${t.conversionPct.toFixed(1)}% conversion rate — and ${RUPEES.format(t.revenue)} in revenue.`,
    `Revenue in the back half of the period is ${dir} ${magnitude}% against the front half.`,
  ];
  if (leader) {
    sentences.push(
      `${leader.name} is carrying the network at ${RUPEES.format(leader.revenueInr)}.`,
    );
  }

  return {
    summary: sentences.join(' '),
    alerts: buildAlerts(storeId, range, endMs),
    tasks: buildTasks(storeId, range, endMs),
  };
}
