import type {ActivityEvent, HourCell, Kpi, TimePoint} from '@/features/dashboard/types/dashboard';
import type {RangeKey} from '@/shared/types/api';
import {createRng} from '@/shared/mock/rng';
import {storeScale} from '@/features/stores/mock/stores.mock';

export const RANGE_DAYS: Record<RangeKey, number> = {
  '7d': 7,
  '30d': 30,
  '90d': 90,
  mtd: 21,
  ytd: 120,
  // Placeholder. Query only carries the RangeKey, so a custom window's real
  // span is not visible to the generators — swap for (end - start) in days
  // once parseQuery threads the dates through.
  custom: 30,
};

/**
 * Fixtures are shaped, not uniform. Retail footfall has a weekly rhythm —
 * weekends run hot, Tuesdays run cold — and a dashboard rendered against flat
 * noise looks fine while hiding the fact that nothing reads as a trend. These
 * curves make the charts honest to look at.
 */
function dayFactor(dayOfWeek: number): number {
  // 0 = Mon … 6 = Sun
  return [0.92, 0.86, 0.95, 1.05, 1.25, 1.4, 1.18][dayOfWeek];
}

export function buildTimeseries(
  range: RangeKey,
  storeId: string,
  endMs: number,
): TimePoint[] {
  const days = RANGE_DAYS[range];
  // The window's end day is part of the seed. Without it, the current and
  // previous periods draw the identical random sequence, so every KPI delta
  // collapses into an artifact of weekday alignment — and all four move the
  // same direction at once, which is exactly what a real dashboard never does.
  // Bucketing to the day keeps it deterministic within a day.
  const endDay = Math.floor(endMs / 86400000);
  const rng = createRng('timeseries', storeId, range, endDay);
  // Scale comes from the store roster, not a flat constant. With a flat base
  // every store produced near-identical numbers and the comparison view's
  // "Top"/"Lowest" badges were effectively random.
  const base = 180 * storeScale(storeId);

  return Array.from({length: days}, (_, i) => {
    const date = new Date(endMs - (days - 1 - i) * 86400000);
    const dow = (date.getUTCDay() + 6) % 7;
    // Mild upward drift so a 90-day view has something to say.
    const drift = 1 + (i / days) * 0.18;
    const visitors = Math.round(
      base * dayFactor(dow) * drift * rng.float(0.92, 1.08),
    );
    const convRate = rng.float(0.17, 0.27);
    const purchases = Math.round(visitors * convRate);
    const avgBasket = rng.float(280, 520);

    return {
      t: date.toISOString().slice(0, 10),
      visitors,
      purchases,
      revenue: Math.round(purchases * avgBasket),
      conversion: Number((convRate * 100).toFixed(1)),
    };
  });
}

function sum(points: TimePoint[], key: keyof TimePoint): number {
  return points.reduce((a, p) => a + (p[key] as number), 0);
}

export function buildKpis(
  range: RangeKey,
  storeId: string,
  endMs: number,
): Kpi[] {
  const days = RANGE_DAYS[range];
  const current = buildTimeseries(range, storeId, endMs);
  // The comparison window is the immediately preceding period of equal length,
  // which is what "vs previous" has to mean for the delta to be meaningful.
  const previous = buildTimeseries(range, storeId, endMs - days * 86400000);

  const delta = (a: number, b: number) =>
    b === 0 ? 0 : Number((((a - b) / b) * 100).toFixed(1));

  const trend = (key: keyof TimePoint) =>
    current.slice(-14).map((p) => ({t: p.t, v: p[key] as number}));

  const rng = createRng('rewards', storeId, range);
  const activeRewards = rng.int(6, 14);

  return [
    {
      id: 'visitors',
      label: 'Visitors',
      value: sum(current, 'visitors'),
      unit: 'count',
      deltaPct: delta(sum(current, 'visitors'), sum(previous, 'visitors')),
      isRiseGood: true,
      trend: trend('visitors'),
    },
    {
      id: 'purchases',
      label: 'Purchases',
      value: sum(current, 'purchases'),
      unit: 'count',
      deltaPct: delta(sum(current, 'purchases'), sum(previous, 'purchases')),
      isRiseGood: true,
      trend: trend('purchases'),
    },
    {
      id: 'revenue',
      label: 'Revenue',
      value: sum(current, 'revenue'),
      unit: 'inr',
      deltaPct: delta(sum(current, 'revenue'), sum(previous, 'revenue')),
      isRiseGood: true,
      trend: trend('revenue'),
    },
    {
      id: 'activeRewards',
      label: 'Active rewards',
      value: activeRewards,
      unit: 'count',
      deltaPct: Number(rng.float(-12, 18).toFixed(1)),
      isRiseGood: true,
      trend: trend('conversion'),
    },
  ];
}

export function buildPeakHours(storeId: string): HourCell[] {
  const rng = createRng('peak', storeId);
  const cells: HourCell[] = [];
  for (let day = 0; day < 7; day++) {
    for (let hour = 0; hour < 24; hour++) {
      // Shops are shut overnight — a heatmap that glows at 04:00 is fiction.
      if (hour < 8 || hour > 22) {
        cells.push({day, hour, value: 0});
        continue;
      }
      // Two humps: lunch and evening, with the evening peak dominant.
      const lunch = Math.exp(-((hour - 13) ** 2) / 4);
      const evening = Math.exp(-((hour - 19) ** 2) / 6) * 1.6;
      const value = Math.round(
        (lunch + evening) * dayFactor(day) * 40 * rng.float(0.85, 1.15),
      );
      cells.push({day, hour, value});
    }
  }
  return cells;
}

const ACTIVITY: {kind: ActivityEvent['kind']; title: string; detail: string}[] =
  [
    {
      kind: 'reward_redeemed',
      title: 'Free Coffee redeemed',
      detail: '120 LYTs · Indiranagar Flagship',
    },
    {
      kind: 'purchase',
      title: 'Purchase completed',
      detail: '₹840 · 2 items · Koramangala',
    },
    {
      kind: 'staff_checked_in',
      title: 'Anita R checked in',
      detail: 'Morning shift · Whitefield',
    },
    {
      kind: 'reward_expired',
      title: 'Combo 20% expired',
      detail: 'Claimed 214 times · used 61',
    },
    {
      kind: 'store_opened',
      title: 'Jubilee Hills opened',
      detail: '09:00 · 3 staff on shift',
    },
  ];

/**
 * 48 events, not 12.
 *
 * The dashboard shows six of these and the Activity page shows the rest, so
 * the fixture has to be long enough for "View all" to lead somewhere. Callers
 * cap what they render — the endpoint returns the history.
 *
 * The gap is ACCUMULATED rather than drawn per index. `endMs - i * rng.int(…)`
 * redraws the spacing on every row, so row 3 could land closer to now than
 * row 2 and the feed arrived out of order — invisible at 12 rows on a panel
 * with no ordering claim, obvious on a page headed "newest first".
 */
export function buildActivity(storeId: string, endMs: number): ActivityEvent[] {
  const rng = createRng('activity', storeId);
  let minutesAgo = 0;

  return Array.from({length: 48}, (_, i) => {
    const tpl = ACTIVITY[i % ACTIVITY.length];
    if (i > 0) minutesAgo += rng.int(6, 40);
    return {
      id: `a${i}`,
      at: new Date(endMs - minutesAgo * 60000).toISOString(),
      kind: tpl.kind,
      title: tpl.title,
      detail: tpl.detail,
      storeId,
    };
  });
}
