/**
 * Pure data-transform utilities for the map view (M2-T09).
 * No leaflet imports — these functions are unit-testable in jsdom.
 */

import type { LocationRecord, PooRecord } from '../records'

/** A heat point for L.heatLayer: [lat, lng, intensity]. */
export type HeatPoint = [number, number, number]

/** Map point with attached source record for click-to-edit. */
export interface LocationMapPoint {
  lat: number
  lng: number
  record: LocationRecord
}

export interface PooMapPoint {
  lat: number
  lng: number
  record: PooRecord
}

// ---------------------------------------------------------------------------
// Transforms
// ---------------------------------------------------------------------------

/**
 * Convert location records to heat points.
 * All points get intensity=1; callers can adjust if needed.
 */
export function locationsToHeatPoints(records: LocationRecord[]): HeatPoint[] {
  return records.map((r) => [r.latitude, r.longitude, 1])
}

/**
 * Convert poo records to heat points.
 */
export function pooToHeatPoints(records: PooRecord[]): HeatPoint[] {
  return records.map((r) => [r.latitude, r.longitude, 1])
}

/**
 * Peak number of 2D coordinates that fall into the same `cellSize`-sized grid
 * cell. Pure + leaflet-free so it is unit-testable.
 *
 * Used by the map heat normalization: project the VISIBLE points to screen
 * pixels (in the map component), then this returns the densest pixel cell's
 * count, which becomes leaflet.heat's `max`. With maxZoom:0 (intensity factor
 * f=1) the accumulated per-cell value equals this count, so the densest visible
 * cluster maps to the hot color — recomputed on every zoom/pan so it always
 * normalizes within the current viewport. Returns at least 1.
 */
export function peakGridCount(coords: Array<[number, number]>, cellSize: number): number {
  if (coords.length === 0) return 1
  const g = Math.max(1, cellSize)
  const counts = new Map<string, number>()
  let peak = 1
  for (const [x, y] of coords) {
    const key = `${Math.floor(x / g)}:${Math.floor(y / g)}`
    const next = (counts.get(key) ?? 0) + 1
    counts.set(key, next)
    if (next > peak) peak = next
  }
  return peak
}

/**
 * Convert location records to map points (for scatter layer).
 */
export function locationsToMapPoints(records: LocationRecord[]): LocationMapPoint[] {
  return records.map((r) => ({ lat: r.latitude, lng: r.longitude, record: r }))
}

/**
 * Convert poo records to map points (for scatter layer).
 */
export function pooToMapPoints(records: PooRecord[]): PooMapPoint[] {
  return records.map((r) => ({ lat: r.latitude, lng: r.longitude, record: r }))
}

// ---------------------------------------------------------------------------
// Client-side time-window filter (for poo records — the endpoint has no server filter)
// ---------------------------------------------------------------------------

/**
 * Filter poo records to those whose timestamp falls within [start, end] (inclusive).
 * start and end are ISO8601 strings (e.g. "2026-01-01T00:00:00Z").
 * If start or end is null, that bound is open (no filtering on that side).
 */
export function filterPooByTimeWindow(
  records: PooRecord[],
  start: string | null,
  end: string | null,
): PooRecord[] {
  if (!start && !end) return records
  return records.filter((r) => {
    const ts = r.timestamp
    if (start && ts < start) return false
    if (end && ts > end) return false
    return true
  })
}

// ---------------------------------------------------------------------------
// Default time window helpers
// ---------------------------------------------------------------------------

/** Returns ISO8601 string for N days ago from now (UTC). */
export function daysAgoISO(days: number): string {
  const d = new Date()
  d.setUTCDate(d.getUTCDate() - days)
  return d.toISOString()
}

/** Returns ISO8601 string for now (UTC). */
export function nowISO(): string {
  return new Date().toISOString()
}

/** Compute a bounding center from an array of lat/lng points. Returns null if empty. */
export function computeCenter(
  points: Array<{ lat: number; lng: number }>,
): [number, number] | null {
  if (points.length === 0) return null
  const sumLat = points.reduce((s, p) => s + p.lat, 0)
  const sumLng = points.reduce((s, p) => s + p.lng, 0)
  return [sumLat / points.length, sumLng / points.length]
}

// ---------------------------------------------------------------------------
// Quick time-range presets + window shifting (Grafana-style)
// ---------------------------------------------------------------------------

const HOUR_MS = 3_600_000
const DAY_MS = 24 * HOUR_MS

/** A quick-range preset: a label + a span in milliseconds (month/year approximated). */
export interface TimePreset {
  value: string
  label: string
  spanMs: number
}

export const TIME_PRESETS: TimePreset[] = [
  { value: '24h', label: 'Past 24 hours', spanMs: 24 * HOUR_MS },
  { value: '1w', label: 'Past 1 week', spanMs: 7 * DAY_MS },
  { value: '2w', label: 'Past 2 weeks', spanMs: 14 * DAY_MS },
  { value: '1mo', label: 'Past 1 month', spanMs: 30 * DAY_MS },
  { value: '6mo', label: 'Past 6 months', spanMs: 182 * DAY_MS },
  { value: '1y', label: 'Past 1 year', spanMs: 365 * DAY_MS },
  { value: '5y', label: 'Past 5 years', spanMs: 5 * 365 * DAY_MS },
]

/** ISO8601 with second precision, no milliseconds: "YYYY-MM-DDTHH:MM:SSZ". */
function isoSeconds(d: Date): string {
  return d.toISOString().slice(0, 19) + 'Z'
}

/**
 * Compute a [start, end] window of width `spanMs` ending at `now`.
 * Used when the user picks a quick-range preset.
 */
export function presetRange(spanMs: number, now: Date = new Date()): { start: string; end: string } {
  return { start: isoSeconds(new Date(now.getTime() - spanMs)), end: isoSeconds(now) }
}

/**
 * Shift a [start, end] window by its OWN span. direction = -1 moves earlier
 * (back in time), +1 moves later. The window width is preserved.
 */
export function shiftRange(
  startISO: string,
  endISO: string,
  direction: -1 | 1,
): { start: string; end: string } {
  const startMs = Date.parse(startISO)
  const endMs = Date.parse(endISO)
  const span = endMs - startMs
  return {
    start: isoSeconds(new Date(startMs + direction * span)),
    end: isoSeconds(new Date(endMs + direction * span)),
  }
}
