/**
 * Pure date-range types and helpers shared by client components and the
 * server-side analytics service.
 *
 * Lives outside `src/lib/services/analytics.service.ts` because that file
 * imports the DB driver (`postgres`) which can't be bundled into client
 * components - see Next.js "Module not found: net" build error.
 *
 * No DB / no IO / no React.
 */

/**
 * Preset date ranges used by the dashboard's quick-pick tabs.
 */
export type PresetDateRange = '7d' | '30d' | '90d' | '1y' | 'today';

/**
 * A custom date range expressed as a pair of ISO date strings (YYYY-MM-DD).
 * The lower bound is inclusive at 00:00; the upper bound is inclusive at
 * 23:59:59.999 (resolved inside `rangeToBounds`).
 */
export interface CustomDateRange {
  kind: 'custom';
  from: string; // ISO YYYY-MM-DD
  to: string; // ISO YYYY-MM-DD
}

export type DateRange = PresetDateRange | CustomDateRange;

export const ALL_RANGES: readonly PresetDateRange[] = ['today', '7d', '30d', '90d', '1y'] as const;

export function isCustomRange(range: DateRange): range is CustomDateRange {
  return typeof range === 'object' && range.kind === 'custom';
}

/**
 * Filename-safe slug for the active range. Used by chart exports so a
 * PNG/CSV download lands as `pipeline-funnel-30d.png` rather than
 * `pipeline-funnel-[object Object].png` (which is what a raw template
 * literal does to a CustomDateRange - Chrome then strips the name and
 * the file falls back to the blob URL's UUID with no extension).
 */
export function rangeToSlug(range: DateRange): string {
  if (isCustomRange(range)) return `${range.from}_${range.to}`;
  return range;
}

/**
 * Inverse of rangeToSlug - parses a `?range=<slug>` query-string value
 * back into a typed DateRange. Returns null on garbage input so callers
 * can fall through to their "no range" default rather than 400ing.
 */
export function parseRangeSlug(slug: string): DateRange | null {
  if (ALL_RANGES.includes(slug as PresetDateRange)) return slug as PresetDateRange;
  // Custom: `YYYY-MM-DD_YYYY-MM-DD`. Both halves must look like ISO dates;
  // anything else is malformed.
  const m = /^(\d{4}-\d{2}-\d{2})_(\d{4}-\d{2}-\d{2})$/.exec(slug);
  if (!m) return null;
  return { kind: 'custom', from: m[1]!, to: m[2]! };
}

/**
 * Resolve any DateRange (preset or custom) to a concrete {from, to} pair.
 * - Preset ranges anchor `to` at "now" and `from` at `now - N days`.
 * - Custom ranges use the operator-supplied dates verbatim, with `to`
 *   normalized to end-of-day so a same-day range still includes that day.
 */
export function rangeToBounds(range: DateRange): { from: Date; to: Date } {
  if (isCustomRange(range)) {
    return {
      from: new Date(`${range.from}T00:00:00.000Z`),
      to: new Date(`${range.to}T23:59:59.999Z`),
    };
  }
  const now = Date.now();
  const days = rangeToDays(range);
  return { from: new Date(now - days * 86_400_000), to: new Date(now) };
}

export function rangeToDays(range: PresetDateRange): number {
  switch (range) {
    case 'today':
      return 1;
    case '7d':
      return 7;
    case '30d':
      return 30;
    case '90d':
      return 90;
    case '1y':
      return 365;
  }
}

/**
 * Number of days a range spans (rounded up). Useful for sizing chart axes.
 */
export function rangeSpanDays(range: DateRange): number {
  if (isCustomRange(range)) {
    const { from, to } = rangeToBounds(range);
    return Math.max(1, Math.ceil((to.getTime() - from.getTime()) / 86_400_000));
  }
  return rangeToDays(range);
}

/**
 * Given a concrete {from, to} window, return the equal-length window
 * immediately preceding it — used for "this period vs prior period"
 * comparison on report KPIs. The prior window is contiguous (its `to`
 * equals the current window's `from`) and exactly the same duration, so
 * a 30-day current window compares against the prior 30 days.
 *
 * Pure: takes Dates in, returns fresh Dates, never mutates the input.
 */
export function previousPeriodBounds(bounds: { from: Date; to: Date }): {
  from: Date;
  to: Date;
} {
  const lengthMs = bounds.to.getTime() - bounds.from.getTime();
  return {
    from: new Date(bounds.from.getTime() - lengthMs),
    to: new Date(bounds.from.getTime()),
  };
}
