<script lang="ts">
  import { api } from '$lib/api';
  import ThroughputComposer from '$lib/components/throughput/ThroughputComposer.svelte';
  import ThroughputDeleteDialog from '$lib/components/throughput/ThroughputDeleteDialog.svelte';
  import ThroughputHistory from '$lib/components/throughput/ThroughputHistory.svelte';
  import type {
    ThroughputEntry,
    ThroughputEntryListParams,
    ThroughputProduct,
    ThroughputQuantityType
  } from '$lib/types';
  import ThroughputSuccessOverlay from '$lib/components/throughput/ThroughputSuccessOverlay.svelte';
  import ThroughputSummary from '$lib/components/throughput/ThroughputSummary.svelte';
  import {
    MIX_RANGES,
    addDays,
    ausToday,
    buildConfetti,
    compareDate,
    compareText,
    isStockEntry,
    startOfWeekMonday,
    toISODate,
    type SortDirection,
    type SortKey
  } from '$lib/components/throughput/utils';
  import { formatDate as formatDisplayDate, formatLocaleNumber, toNum } from '$lib/format';

  let { data } = $props<{
    data: { entries: ThroughputEntry[]; statsEntries: ThroughputEntry[]; products: ThroughputProduct[] };
  }>();

  let entries = $state<ThroughputEntry[]>([]);
  let entriesInitialized = $state(false);
  // Separate, unfiltered window (~6 weeks) that backs the hero cards so the
  // "Find past entries" filters on the log never distort Today / This week.
  let statsEntries = $state<ThroughputEntry[]>([]);
  let statsInitialized = $state(false);
  const products = $derived(data.products ?? []);
  const PAGE_SIZE = 20;
  let sortKey = $state<SortKey>('date');
  let sortDirection = $state<SortDirection>('desc');
  let page = $state(1);

  $effect(() => {
    if (!entriesInitialized) {
      entries = data.entries ?? [];
      entriesInitialized = true;
    }
  });

  $effect(() => {
    if (!statsInitialized) {
      statsEntries = data.statsEntries ?? data.entries ?? [];
      statsInitialized = true;
    }
  });

  // Keep the hero-card dataset in sync as the operator adds, edits, or removes
  // runs without needing a round-trip to reload the 6-week window.
  function upsertStatsEntry(entry: ThroughputEntry) {
    const idx = statsEntries.findIndex((e) => e.id === entry.id);
    if (idx === -1) {
      statsEntries = [entry, ...statsEntries];
    } else {
      statsEntries = statsEntries.map((e) => (e.id === entry.id ? entry : e));
    }
  }

  function removeStatsEntry(id: number) {
    statsEntries = statsEntries.filter((e) => e.id !== id);
  }

  // The destination shown in the log: an order (with job number), stock, or a
  // split across both.
  function destinationOf(entry: ThroughputEntry): { label: string; detail: string | null } {
    const unit = entry.quantity_type === 'bags' ? 'bags' : 'kg';
    if (entry.for_order && entry.for_stock) {
      const stock = entry.stock_quantity != null ? `${formatNumber(entry.stock_quantity, 1)} ${unit} to stock` : 'split';
      const job = entry.job_number ? `Order ${entry.job_number}` : 'Order';
      return { label: 'Split', detail: `${job} · ${stock}` };
    }
    if (entry.for_order) {
      return { label: 'Order', detail: entry.job_number ? `Job ${entry.job_number}` : null };
    }
    if (isStockEntry(entry)) {
      return { label: 'Stock', detail: null };
    }
    return { label: '—', detail: null };
  }

  // ── Inline "spreadsheet" add row ──────────────────────────────
  const today = toISODate(ausToday());
  let nDate = $state(today);
  let nProductId = $state('');
  let nQuantity = $state('');
  let nType = $state<ThroughputQuantityType>('bags');
  let nBagSize = $state('');
  let nStaff = $state('');
  let nNotes = $state('');
  // Destination checkpoints: a run can go to a client order, to stock, or both.
  let nForOrder = $state(false);
  let nForStock = $state(false);
  let nJobNumber = $state('');
  let nStockQty = $state('');
  let showNote = $state(false);
  let saving = $state(false);
  let addError = $state('');
  let highlightId = $state<number | null>(null);
  // When set, the composer is editing an existing run rather than adding one.
  let editingId = $state<number | null>(null);
  let composer = $state<HTMLElement | null>(null);
  let deletingId = $state<number | null>(null);

  // A brief centred "Added" confirmation with a confetti burst after a run is
  // logged, so a fast operator gets unmistakable feedback even with their eyes
  // on the keyboard. Auto-dismisses; pointer-events stay off so it never blocks
  // the next entry.
  let showSuccess = $state(false);
  let confetti = $state(buildConfetti());
  let successTimer: ReturnType<typeof setTimeout> | null = null;

  function flashSuccess() {
    confetti = buildConfetti();
    showSuccess = true;
    if (successTimer) clearTimeout(successTimer);
    successTimer = setTimeout(() => {
      showSuccess = false;
    }, 3000);
  }

  // When both checkpoints are ticked the run is split, so we need to know how
  // much goes to stock (the rest belongs to the order).
  const isSplit = $derived(nForOrder && nForStock);

  const selectedNewProduct = $derived(
    nProductId ? products.find((p: ThroughputProduct) => String(p.id) === nProductId) ?? null : null
  );

  // Mirror the product's defaults the way the full form does: prefill the bag
  // size and switch bulka products to kilograms.
  $effect(() => {
    const p = selectedNewProduct;
    if (p) {
      if (!nBagSize && p.default_bag_size != null) nBagSize = String(p.default_bag_size);
      if (p.is_bulka_default) nType = 'kg';
    }
  });

  // Bags: number of bags x kg-per-bag = total kg. Bulka/bulk: the kg amount is
  // the total directly. Returns null until there is enough to compute.
  const addTotalKg = $derived.by(() => {
    const qty = toNum(nQuantity);
    if (qty === null) return null;
    if (nType === 'kg') return qty;
    const bag = toNum(nBagSize);
    return bag === null ? null : qty * bag;
  });

  // Collapse the note field again, discarding anything typed. Lets the operator
  // back out after opening "+ Add a note" without leaving a stray value behind.
  function dismissNote() {
    nNotes = '';
    showNote = false;
  }

  function resetComposer() {
    nProductId = '';
    nQuantity = '';
    nType = 'bags';
    nBagSize = '';
    nNotes = '';
    nForOrder = false;
    nForStock = false;
    nJobNumber = '';
    nStockQty = '';
    showNote = false;
  }

  // Load an existing run back into the composer so it can be corrected in place.
  function startEdit(entry: ThroughputEntry) {
    editingId = entry.id;
    addError = '';
    nDate = entry.production_date;
    nProductId = entry.product_id != null ? String(entry.product_id) : '';
    nQuantity = String(entry.quantity);
    nType = entry.quantity_type;
    nBagSize = entry.bag_size != null ? String(entry.bag_size) : '';
    nStaff = entry.staff_name ?? '';
    nNotes = entry.notes ?? '';
    showNote = Boolean(entry.notes);
    nForOrder = entry.for_order;
    nForStock = entry.for_stock;
    nJobNumber = entry.job_number ?? '';
    nStockQty = entry.stock_quantity != null ? String(entry.stock_quantity) : '';
    composer?.scrollIntoView({ behavior: 'smooth', block: 'start' });
  }

  function cancelEdit() {
    editingId = null;
    addError = '';
    resetComposer();
  }

  // Delete uses an in-app confirmation modal (not the browser's confirm) so the
  // action stays inside the app's design system. `pendingDelete` holds the run
  // awaiting confirmation.
  let pendingDelete = $state<ThroughputEntry | null>(null);
  let deleteDialog = $state<HTMLElement | null>(null);

  function requestDelete(entry: ThroughputEntry) {
    pendingDelete = entry;
  }

  function cancelDelete() {
    pendingDelete = null;
  }

  // Move focus into the dialog when it opens so Escape/keyboard work and screen
  // readers land on it.
  $effect(() => {
    if (pendingDelete) deleteDialog?.focus();
  });

  async function confirmDelete() {
    const entry = pendingDelete;
    if (!entry) return;
    deletingId = entry.id;
    try {
      await api.deleteThroughputEntry(entry.id);
      entries = entries.filter((e) => e.id !== entry.id);
      removeStatsEntry(entry.id);
      if (editingId === entry.id) cancelEdit();
      pendingDelete = null;
    } catch (err) {
      errorMessage = err instanceof Error ? err.message : 'Could not delete this entry. Please try again.';
    } finally {
      deletingId = null;
    }
  }

  async function addEntry() {
    addError = '';
    if (!nProductId) {
      addError = 'Choose a product first.';
      return;
    }
    const qty = toNum(nQuantity);
    if (qty === null || qty < 0) {
      addError = 'Enter how many were packed.';
      return;
    }
    const bag = toNum(nBagSize);
    if (nType === 'bags' && (bag === null || bag <= 0)) {
      addError = 'Enter the bag size in kg.';
      return;
    }

    if (!nForOrder && !nForStock) {
      addError = 'Mark where this run goes: for an order, for stock, or both.';
      return;
    }

    const job = nJobNumber.trim();
    if (nForOrder && !job) {
      addError = 'Enter the job number for the order.';
      return;
    }

    // For a split, the operator records how much goes to stock; the remainder
    // belongs to the order. Whole runs (stock-only or order-only) don't need it.
    let stockQty: number | null = null;
    if (isSplit) {
      stockQty = toNum(nStockQty);
      if (stockQty === null || stockQty <= 0) {
        addError = `Enter how much goes to stock (in ${nType === 'bags' ? 'bags' : 'kg'}).`;
        return;
      }
      if (stockQty >= qty) {
        addError = 'Stock amount must be less than the total packed for a split.';
        return;
      }
    }

    const payload = {
      production_date: nDate,
      product_id: Number(nProductId),
      product_name_snapshot: selectedNewProduct?.name ?? '',
      bag_size: nType === 'bags' ? bag : null,
      quantity: qty,
      quantity_type: nType,
      for_order: nForOrder,
      for_stock: nForStock,
      job_number: nForOrder ? job : null,
      stock_quantity: stockQty,
      staff_name: nStaff.trim() || null,
      notes: nNotes.trim() || null
    };

    saving = true;
    try {
      if (editingId != null) {
        const updated = await api.updateThroughputEntry(editingId, payload);
        entries = entries.map((e) => (e.id === updated.id ? updated : e));
        upsertStatsEntry(updated);
        highlightId = updated.id;
        editingId = null;
      } else {
        const created = await api.createThroughputEntry(payload);
        entries = [created, ...entries];
        upsertStatsEntry(created);
        page = 1;
        highlightId = created.id;
        flashSuccess();
      }
      const flashed = highlightId;
      setTimeout(() => {
        if (highlightId === flashed) highlightId = null;
      }, 1800);
      // Reset for the next entry, keeping date and staff for fast repeats.
      resetComposer();
    } catch (err) {
      addError = err instanceof Error ? err.message : 'Could not save. Please try again.';
    } finally {
      saving = false;
    }
  }

  let dateFrom = $state<string>('');
  let dateTo = $state<string>('');
  let productFilter = $state<string>('');
  let staffFilter = $state<string>('');
  let typeFilter = $state<'' | ThroughputQuantityType>('');
  let isLoading = $state(false);
  let errorMessage = $state('');
  let showFilters = $state(false);

  const filtersActive = $derived(
    Boolean(dateFrom || dateTo || productFilter || staffFilter.trim() || typeFilter)
  );

  async function applyFilters() {
    isLoading = true;
    errorMessage = '';
    try {
      const params: ThroughputEntryListParams = { limit: 200 };
      if (dateFrom) {
        params.date_from = dateFrom;
      } else if (!filtersActive) {
        // No filters at all: keep the default "last 30 days" window.
        const recent = new Date();
        recent.setDate(recent.getDate() - 30);
        params.date_from = recent.toISOString().slice(0, 10);
      }
      if (dateTo) params.date_to = dateTo;
      if (productFilter) params.product_id = Number(productFilter);
      if (staffFilter.trim()) params.staff_name = staffFilter.trim();
      if (typeFilter) params.quantity_type = typeFilter;
      entries = await api.throughputEntries(params);
      page = 1;
    } catch (err) {
      errorMessage = err instanceof Error ? err.message : 'Could not load entries. Please try again.';
    } finally {
      isLoading = false;
    }
  }

  function clearFilters() {
    dateFrom = '';
    dateTo = '';
    productFilter = '';
    staffFilter = '';
    typeFilter = '';
    applyFilters();
  }

  // ── Hero stats: today, this week (Mon–Sun), and the trailing 4-week average ──
  const heroStats = $derived.by(() => {
    const now = ausToday();
    const todayStr = toISODate(now);
    const thisMonday = startOfWeekMonday(now);
    // Index 0 = current week, 1–4 = the four completed weeks before it.
    const weekStarts = [0, 1, 2, 3, 4].map((i) => addDays(thisMonday, -7 * i));
    const earliest = weekStarts[4];

    let today = 0;
    const weekTotals = [0, 0, 0, 0, 0];
    for (const entry of statsEntries) {
      const kg = entry.calculated_kg || 0;
      if (entry.production_date === todayStr) today += kg;
      const [y, m, day] = entry.production_date.split('-').map(Number);
      if (!y || !m || !day) continue;
      const d = new Date(y, m - 1, day);
      if (d < earliest) continue;
      for (let i = 0; i < 5; i++) {
        if (d >= weekStarts[i]) {
          weekTotals[i] += kg;
          break;
        }
      }
    }

    const priorFour = weekTotals.slice(1);
    const avgFourWeek = priorFour.reduce((sum, value) => sum + value, 0) / 4;
    return { today, thisWeek: weekTotals[0], avgFourWeek };
  });

  // "9 Jun – 15 Jun"-style range for the current Mon–Sun week, shown as the
  // This week card's subtitle (AUS time).
  const weekRangeLabel = $derived.by(() => {
    const monday = startOfWeekMonday(ausToday());
    const sunday = addDays(monday, 6);
    const fmt = (d: Date) =>
      formatDisplayDate(toISODate(d), { day: 'numeric', month: 'short' }, 'en-AU');
    return `${fmt(monday)} – ${fmt(sunday)}`;
  });

  // ── Customer split: Horse Mix (PHF Horsemix) vs Grain Mix (everyone else) ──
  // The entry carries a product id; the customer lives on the product record.
  const productClientById = $derived.by(() => {
    const map = new Map<number, string | null>();
    for (const p of products as ThroughputProduct[]) map.set(p.id, p.client_name);
    return map;
  });

  // PHF Horsemix is the only "horse mix" customer; tolerate spelling variants
  // ("PHF Horsemix" / "PHF Horse Mix(es)") by matching on the two key words.
  function isHorseMixClient(name: string | null | undefined): boolean {
    if (!name) return false;
    const norm = name.toLowerCase();
    return norm.includes('phf') && norm.includes('horse');
  }

  let mixRangeKey = $state<(typeof MIX_RANGES)[number]['key']>('4w');
  const mixRange = $derived(MIX_RANGES.find((r) => r.key === mixRangeKey) ?? MIX_RANGES[1]);

  const mixTotals = $derived.by(() => {
    const cutoff = toISODate(addDays(ausToday(), -(mixRange.days - 1)));
    let horse = 0;
    let grain = 0;
    for (const entry of statsEntries) {
      if (entry.production_date < cutoff) continue;
      const kg = entry.calculated_kg || 0;
      const client = entry.product_id != null ? productClientById.get(entry.product_id) ?? null : null;
      if (isHorseMixClient(client)) horse += kg;
      else grain += kg;
    }
    return { horse, grain };
  });

  function formatDate(value: string) {
    return formatDisplayDate(value, { day: '2-digit', month: 'short', year: 'numeric' }, 'en-AU');
  }

  function formatNumber(value: number | null | undefined, digits = 0) {
    return formatLocaleNumber(value, digits, 'en-AU');
  }

  function packedMain(entry: ThroughputEntry) {
    if (entry.quantity_type === 'bags') {
      return `${formatNumber(entry.quantity)} ${entry.quantity === 1 ? 'bag' : 'bags'}`;
    }
    return `${formatNumber(entry.quantity, 1)} kg`;
  }

  function packedDetail(entry: ThroughputEntry) {
    if (entry.quantity_type === 'bags') {
      const each = entry.bag_size ? `${formatNumber(entry.bag_size, 2)} kg each` : null;
      const total = `${formatNumber(entry.calculated_kg)} kg total`;
      return each ? `${each} · ${total}` : total;
    }
    return 'Bulka / bulk';
  }

  function toggleSort(key: SortKey) {
    if (sortKey === key) {
      sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
      return;
    }

    sortKey = key;
    sortDirection = key === 'date' ? 'desc' : 'asc';
    page = 1;
  }

  const sortedEntries = $derived.by(() => {
    const direction = sortDirection === 'asc' ? 1 : -1;
    return [...entries].sort((a, b) => {
      let result = 0;

      if (sortKey === 'date') {
        result = compareDate(a.production_date, b.production_date);
      } else if (sortKey === 'product') {
        result = compareText(a.product_name_snapshot, b.product_name_snapshot);
      } else if (sortKey === 'packed') {
        result = (a.calculated_kg ?? 0) - (b.calculated_kg ?? 0);
        if (result === 0) result = (a.quantity ?? 0) - (b.quantity ?? 0);
      } else if (sortKey === 'total') {
        result = (a.calculated_kg ?? 0) - (b.calculated_kg ?? 0);
      } else if (sortKey === 'staff') {
        result = compareText(a.staff_name, b.staff_name);
      } else if (sortKey === 'destination') {
        const aDest = destinationOf(a);
        const bDest = destinationOf(b);
        result = compareText(`${aDest.label} ${aDest.detail ?? ''}`, `${bDest.label} ${bDest.detail ?? ''}`);
      } else if (sortKey === 'notes') {
        result = compareText(a.notes, b.notes);
      }

      if (result === 0) {
        result = (a.id ?? 0) - (b.id ?? 0);
      }

      return result * direction;
    });
  });

  const totalPages = $derived(Math.max(1, Math.ceil(sortedEntries.length / PAGE_SIZE)));
  const paginatedEntries = $derived(sortedEntries.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE));
  const pageStart = $derived(sortedEntries.length === 0 ? 0 : (page - 1) * PAGE_SIZE + 1);
  const pageEnd = $derived(Math.min(page * PAGE_SIZE, sortedEntries.length));

  $effect(() => {
    if (page > totalPages) page = totalPages;
  });
</script>

<section class="throughput">
  <ThroughputSummary
    {today}
    {weekRangeLabel}
    {heroStats}
    {mixTotals}
    bind:mixRangeKey
    {formatDate}
    {formatNumber}
  />

  <section class="throughput-shell">
    <ThroughputComposer
      {products}
      {editingId}
      {saving}
      bind:nDate
      bind:nProductId
      bind:nQuantity
      bind:nType
      bind:nBagSize
      bind:nStaff
      bind:nNotes
      bind:nForOrder
      bind:nForStock
      bind:nJobNumber
      bind:nStockQty
      bind:showNote
      addError={addError}
      isSplit={isSplit}
      addTotalKg={addTotalKg}
      {formatNumber}
      onSubmit={addEntry}
      onDismissNote={dismissNote}
      onCancelEdit={cancelEdit}
      bind:composerRef={composer}
    />

    <ThroughputHistory
      {products}
      {filtersActive}
      bind:showFilters
      bind:dateFrom
      bind:dateTo
      bind:productFilter
      bind:staffFilter
      bind:typeFilter
      {isLoading}
      {errorMessage}
      {sortKey}
      {highlightId}
      {deletingId}
      {sortedEntries}
      {paginatedEntries}
      bind:page
      {totalPages}
      {pageStart}
      {pageEnd}
      {formatDate}
      {formatNumber}
      {packedMain}
      {packedDetail}
      {destinationOf}
      onApplyFilters={applyFilters}
      onClearFilters={clearFilters}
      onToggleSort={toggleSort}
      onStartEdit={startEdit}
      onRequestDelete={requestDelete}
    />
  </section>
</section>

{#if pendingDelete}
  <ThroughputDeleteDialog
    pendingDelete={pendingDelete}
    {deletingId}
    {formatDate}
    onCancel={cancelDelete}
    onConfirm={confirmDelete}
    bind:dialogRef={deleteDialog}
  />
{/if}

{#if showSuccess}
  <ThroughputSuccessOverlay {confetti} />
{/if}

<style>
  .throughput {
    display: grid;
    gap: 1.6rem;
    padding: 0.4rem 0 2.5rem;
  }

  .throughput-shell {
    display: grid;
    gap: 1rem;
  }
</style>

