import {
  findAllStageHistory, findAllDispatchEvents, findAllCancelEvents, findAllOrderProductsForCompletion,
  findAllOrderCreationEvents,
} from '../repositories/log.repo.js';
import { getStages } from './stage.service.js';

export type LogEventType = 'order_created' | 'stage_change' | 'returned' | 'dispatched' | 'partially_dispatched' | 'cancelled' | 'partially_cancelled' | 'order_completed';

export interface StageLogRow {
  id: string;
  changed_at: string;
  event_type: LogEventType;
  order_id: string;
  order_no: string;
  jo_no: string;
  customer_name: string;
  item_name: string;
  category_name: string;
  metal_name: string;
  purity_name: string;
  quantity: number;
  pending_quantity: number;
  priority: string;
  from_stage: number;
  to_stage: number;
  employee_name: string;
  // Only populated for 'returned' rows — the reason entered when the return
  // was requested.
  note?: string;
}

function stageIndex(stageId: string): number {
  return getStages().findIndex((s) => s.id === stageId);
}

// Every order_stage_history row records the stage a line just entered, not
// a from→to pair — so the transition for each row is derived from whichever
// stage its own order_product was sitting at right before (rows arrive
// pre-sorted by order_product_id then entered_at, so that's just "the
// previous row for the same line", or -1/"Not Started" for its first row).
function buildStageChangeRows(rows: Awaited<ReturnType<typeof findAllStageHistory>>): StageLogRow[] {
  const result: StageLogRow[] = [];
  let prevOrderProductId: string | null = null;
  let prevStage = -1;

  for (const row of rows) {
    if (row.order_product_id !== prevOrderProductId) {
      prevStage = -1;
      prevOrderProductId = row.order_product_id;
    }
    const toStage = stageIndex(row.stage_id);
    // orderReturn.service.ts's approve() marks the stage-history row it
    // inserts with this private "[RETURN] " prefix so it shows up here as
    // its own distinct "Returned" event instead of blending into an
    // ordinary stage change.
    const isReturn = row.remarks?.startsWith('[RETURN] ') ?? false;
    result.push({
      id: row.id,
      changed_at: row.entered_at.toISOString(),
      event_type: isReturn ? 'returned' : 'stage_change',
      order_id: row.order_id,
      order_no: row.order_no,
      jo_no: row.jo_no ?? '—',
      customer_name: row.customer_name,
      item_name: row.item_name,
      category_name: row.category_name ?? '—',
      metal_name: row.metal_name ?? '—',
      purity_name: row.purity_name ?? '—',
      quantity: row.quantity,
      pending_quantity: row.quantity - row.dispatched_quantity - row.cancelled_quantity,
      priority: row.priority,
      from_stage: prevStage,
      to_stage: toStage,
      employee_name: row.employee_name ?? '—',
      note: isReturn ? row.remarks!.slice('[RETURN] '.length) : undefined,
    });
    prevStage = toStage;
  }

  return result;
}

// A line can be dispatched across several partial batches — each batch is
// its own row here, and whichever batch brings the line's running total up
// to its full dispatchable quantity is the one labeled "dispatched" rather
// than "partially dispatched".
function buildDispatchRows(rows: Awaited<ReturnType<typeof findAllDispatchEvents>>): StageLogRow[] {
  const byLine = new Map<string, typeof rows>();
  for (const row of rows) {
    const list = byLine.get(row.order_product_id) ?? [];
    list.push(row);
    byLine.set(row.order_product_id, list);
  }

  const result: StageLogRow[] = [];
  for (const list of byLine.values()) {
    const sorted = [...list].sort((a, b) => a.dispatch_date.getTime() - b.dispatch_date.getTime());
    const dispatchable = sorted[0]!.line_quantity - sorted[0]!.line_cancelled_quantity;
    let running = 0;
    for (const row of sorted) {
      running += row.quantity;
      const pendingNow = row.line_quantity - row.line_dispatched_quantity - row.line_cancelled_quantity;
      result.push({
        id: row.id,
        changed_at: row.dispatch_date.toISOString(),
        event_type: running >= dispatchable ? 'dispatched' : 'partially_dispatched',
        order_id: row.order_id,
        order_no: row.order_no,
        jo_no: row.jo_no ?? '—',
        customer_name: row.customer_name,
        item_name: row.item_name,
          category_name: row.category_name ?? '—',
        metal_name: row.metal_name ?? '—',
        purity_name: row.purity_name ?? '—',
        quantity: row.quantity,
        pending_quantity: Math.max(0, pendingNow),
        priority: row.priority,
        from_stage: -1,
        to_stage: -1,
        employee_name: row.employee_name ?? '—',
      });
    }
  }
  return result;
}

// Unlike dispatch, there's no per-cancellation history table — a line can be
// partially cancelled more than once before it's fully cancelled, but only
// its current cumulative state (cancelled_quantity/cancel_date/updated_at)
// is ever stored, so this can only ever show one row reflecting the latest
// cancellation action, not each individual partial cancel over time.
function buildCancelRows(rows: Awaited<ReturnType<typeof findAllCancelEvents>>): StageLogRow[] {
  return rows.map((row) => ({
    id: `CXL-${row.order_product_id}`,
    // cancel_date is a DATE-only column (no time-of-day, defaults to
    // midnight) — sorting by it would always place a cancellation before
    // same-day events that have real timestamps. updated_at is bumped by
    // MySQL on the same UPDATE that sets cancel_date, so it reflects the
    // actual moment this happened.
    changed_at: row.updated_at.toISOString(),
    event_type: row.cancelled ? ('cancelled' as const) : ('partially_cancelled' as const),
    order_id: row.order_id,
    order_no: row.order_no,
    jo_no: row.jo_no ?? '—',
    customer_name: row.customer_name,
    item_name: row.item_name,
    category_name: row.category_name ?? '—',
    metal_name: row.metal_name ?? '—',
    purity_name: row.purity_name ?? '—',
    quantity: row.cancelled_quantity,
    pending_quantity: Math.max(0, row.line_quantity - row.line_dispatched_quantity - row.cancelled_quantity),
    priority: row.priority,
    from_stage: -1,
    to_stage: -1,
    employee_name: row.employee_name ?? '—',
  }));
}

// A line is "resolved" once nothing on it is still pending — either it was
// fully dispatched, or whatever wasn't dispatched was cancelled instead. An
// order counts as completed once every line is resolved AND at least one
// line was actually dispatched — an order where every line was cancelled
// outright isn't a "completion", it's just a cancellation (already covered
// by its own cancelled-event rows), so it doesn't get this badge too.
function buildOrderCompletedRows(rows: Awaited<ReturnType<typeof findAllOrderProductsForCompletion>>): StageLogRow[] {
  const byOrder = new Map<string, typeof rows>();
  for (const row of rows) {
    const list = byOrder.get(row.order_id) ?? [];
    list.push(row);
    byOrder.set(row.order_id, list);
  }

  const result: StageLogRow[] = [];
  for (const [orderId, lines] of byOrder.entries()) {
    if (lines.length === 0) continue;
    const resolved = lines.every((l) => l.dispatched || l.cancelled_quantity >= l.quantity);
    const hasDispatch = lines.some((l) => l.dispatched);
    if (!resolved || !hasDispatch) continue;

    let latest: typeof lines[number] | null = null;
    let latestTime = -Infinity;
    for (const line of lines) {
      // dispatch_date/cancel_date are DATE-only (midnight, no time-of-day);
      // updated_at is bumped by MySQL on the same UPDATE that sets either of
      // them, so it's the precise moment this line was actually resolved —
      // needed to correctly rank same-day completions against other logs.
      const t = line.updated_at.getTime();
      if (t > latestTime) {
        latestTime = t;
        latest = line;
      }
    }
    if (!latest || latestTime === -Infinity) continue;

    const totalQuantity = lines.reduce((sum, l) => sum + l.quantity, 0);
    const closedByDispatch = (latest.dispatch_date?.getTime() ?? -Infinity) >= (latest.cancel_date?.getTime() ?? -Infinity);

    result.push({
      id: `DONE-${orderId}`,
      changed_at: new Date(latestTime).toISOString(),
      event_type: 'order_completed',
      order_id: orderId,
      order_no: lines[0]!.order_no,
      jo_no: '—',
      customer_name: lines[0]!.customer_name,
      item_name: 'All items',
      category_name: '—',
      metal_name: '—',
      purity_name: '—',
      quantity: totalQuantity,
      pending_quantity: 0,
      priority: lines[0]!.priority,
      from_stage: -1,
      to_stage: -1,
      employee_name: (closedByDispatch ? latest.dispatched_by_employee_name : latest.cancelled_by_employee_name) ?? '—',
    });
  }
  return result;
}

function buildOrderCreatedRows(rows: Awaited<ReturnType<typeof findAllOrderCreationEvents>>): StageLogRow[] {
  return rows.map((row) => ({
    id: `NEW-${row.order_id}`,
    changed_at: row.created_at.toISOString(),
    event_type: 'order_created' as const,
    order_id: row.order_id,
    order_no: row.order_no,
    jo_no: '—',
    customer_name: row.customer_name,
    item_name: 'All items',
    category_name: '—',
    metal_name: '—',
    purity_name: '—',
    quantity: row.total_quantity,
    pending_quantity: row.total_quantity,
    priority: row.priority,
    from_stage: -1,
    to_stage: -1,
    employee_name: row.taken_by_employee_name ?? '—',
  }));
}

export async function listStageLogs(): Promise<StageLogRow[]> {
  const [creationRows, stageRows, dispatchRows, cancelRows, completionRows] = await Promise.all([
    findAllOrderCreationEvents(),
    findAllStageHistory(),
    findAllDispatchEvents(),
    findAllCancelEvents(),
    findAllOrderProductsForCompletion(),
  ]);

  const result = [
    ...buildOrderCreatedRows(creationRows),
    ...buildStageChangeRows(stageRows),
    ...buildDispatchRows(dispatchRows),
    ...buildCancelRows(cancelRows),
    ...buildOrderCompletedRows(completionRows),
  ];

  result.sort((a, b) => b.changed_at.localeCompare(a.changed_at));
  return result;
}
