import { getConnection } from '../../config/database.js';
import {
  insert as insertReturn, findById, findByIdForUpdate, findPending, findAll as findAllReturns,
  findOpenCountForKpi, findOpen, sumOpenReturnedQtyForLine, markActive, markRejected, markReverted,
  update as updateReturnRow,
  type FindReturnsFilters,
} from '../repositories/orderReturn.repo.js';
import { findByIdForUpdate as findOrderProductForUpdate, applyReturn, applyRevert } from '../repositories/orderProduct.repo.js';
import { insert as insertStageHistory, getCurrentStage, completeStage } from '../repositories/orderStageHistory.repo.js';
import { createWithId } from '../utils/id.js';
import { now } from '../utils/helpers.js';
import { NotFoundError, ConflictError } from '../utils/errors.js';
import { emitter } from '../events/emitter.js';
import { getStages } from './stage.service.js';
import type { OrderReturn } from '../types/index.js';

export interface RequestReturnData {
  order_id: string;
  order_product_id: string;
  returned_quantity: number;
  returned_weight: number;
  reason: string;
  target_stage_id: string;
  returned_date: string;
  next_delivery_date?: string | null;
}

// Shared by submitForRequest() and createDirect() — validates the original
// line is delivered and not soft-deleted, and that the requested quantity
// doesn't exceed what's still returnable (dispatched_quantity minus every
// already-open return on the same line — the double-booking guard). Always
// inserted as 'pending'; createDirect() immediately follows up with
// approve() rather than skipping the pending row entirely, so the full
// requested -> approved lifecycle is still there to log/audit even for an
// Admin's one-step direct create.
//
// Runs the line-lookup, the remaining-quantity check, and the insert inside
// one transaction with the line row-locked (findOrderProductForUpdate) for
// its whole duration — closes the race where two return requests submitted
// within moments of each other (e.g. two different employees, or an Admin's
// createDirect() firing while someone else's request lands) both read the
// same "remaining returnable" figure before either INSERT commits, and both
// get accepted even though only one actually fits. approve() below applies
// the identical lock-then-recheck pattern for the symmetric race at
// approval time (two pending requests on the same line, only one of which
// can actually be approved).
async function insertPendingReturn(data: RequestReturnData, requestedByEmployeeId: string): Promise<string> {
  if (data.returned_quantity <= 0) throw new ConflictError('Returned quantity must be greater than zero');
  if (data.returned_weight <= 0) throw new ConflictError('Returned weight must be greater than zero');
  if (!data.reason?.trim()) throw new ConflictError('A reason is required');

  const stage = getStages().find((s) => s.id === data.target_stage_id);
  if (!stage) throw new ConflictError('Invalid target stage');

  const conn = await getConnection();
  try {
    await conn.beginTransaction();

    const line = await findOrderProductForUpdate(data.order_product_id, conn);
    if (!line || line.order_id !== data.order_id) throw new NotFoundError('Order line not found');
    // Checks *some* quantity is currently delivered, not the fully-dispatched
    // flag — a line that's already had one return approved has dispatched
    // flipped back to false even though part of it may still legitimately be
    // out with the customer and returnable again.
    if (!line.dispatched_quantity || line.dispatched_quantity <= 0) {
      throw new ConflictError('Only a line with delivered quantity can be returned');
    }

    const alreadyOpen = await sumOpenReturnedQtyForLine(line.id, conn);
    const remaining = (line.dispatched_quantity ?? 0) - alreadyOpen;
    if (data.returned_quantity > remaining) {
      throw new ConflictError(`Cannot return ${data.returned_quantity} unit(s) — only ${remaining} unit(s) remain returnable on this line`);
    }

    const { id } = await createWithId('order_returns', 'RET', (id) => insertReturn(id, {
      order_id: data.order_id,
      order_product_id: data.order_product_id,
      returned_quantity: data.returned_quantity,
      returned_weight: data.returned_weight,
      reason: data.reason,
      target_stage_id: data.target_stage_id,
      status: 'pending',
      requested_by_employee_id: requestedByEmployeeId,
      returned_date: data.returned_date,
      next_delivery_date: data.next_delivery_date ?? null,
    }, requestedByEmployeeId, conn), conn);

    await conn.commit();
    return id;
  } catch (err) {
    await conn.rollback();
    throw err;
  } finally {
    conn.release();
  }
}

export async function submitForRequest(data: RequestReturnData, requestedByEmployeeId: string): Promise<{ id: string }> {
  const id = await insertPendingReturn(data, requestedByEmployeeId);
  emitter.emit('activity.create', {
    entity_type: 'order_return', entity_id: id, action: 'return_requested', user_id: requestedByEmployeeId,
  });
  return { id };
}

// Admin-only shortcut: files the same pending request, then immediately runs
// it through approve() — no separate review step for them (mirrors
// order.service.ts's create() direct-approve shortcut for orders.approve
// holders).
export async function createDirect(data: RequestReturnData, adminEmployeeId: string): Promise<OrderReturn> {
  const id = await insertPendingReturn(data, adminEmployeeId);
  emitter.emit('activity.create', {
    entity_type: 'order_return', entity_id: id, action: 'return_requested', user_id: adminEmployeeId,
  });
  return approve(id, adminEmployeeId);
}

export async function listPending(filters: FindReturnsFilters = {}): Promise<OrderReturn[]> {
  return findPending(filters);
}

export async function listAll(filters: FindReturnsFilters = {}): Promise<OrderReturn[]> {
  return findAllReturns(filters);
}

// Backs the dashboard "Returned Items" KPI modal — every open (pending or
// active) return, same set the KPI card itself counts.
export async function listOpen(): Promise<OrderReturn[]> {
  return findOpen();
}

export async function get(id: string): Promise<OrderReturn> {
  const ret = await findById(id);
  if (!ret) throw new NotFoundError('Return not found');
  return ret;
}

export async function getKpiCount(): Promise<number> {
  return findOpenCountForKpi();
}

// Approval is final — the approver never re-edits the submitted values
// (quantity/weight/reason/target stage); if something's wrong they reject
// with a reason and the requester resubmits (see reject() below).
//
// On approval, the return is applied *in place* to the original line — no
// new order_products row. This mirrors dispatchProducts()'s own bookkeeping
// but inverted: the returned quantity/weight is subtracted back off the
// line's dispatched counters (exactly what makes it reappear as
// undelivered), and the line's current stage is closed and re-opened at the
// chosen target stage (the same close-then-insert mechanism advanceStage()
// uses). The line's quantity/weight/size/jo_no/dispatch_date/dispatched_by_*
// fields are never touched here, so the order's nominal contents and the
// *original* delivery's details both stay exactly as they were until the
// line is actually redelivered.
export async function approve(returnId: string, reviewerId: string): Promise<OrderReturn> {
  const conn = await getConnection();
  try {
    await conn.beginTransaction();

    const ret = await findByIdForUpdate(returnId, conn);
    if (!ret) throw new NotFoundError('Return not found');
    if (ret.status !== 'pending') throw new ConflictError('This return has already been reviewed');

    const line = await findOrderProductForUpdate(ret.order_product_id, conn);
    if (!line) throw new NotFoundError('Original order line not found');

    // Re-validate remaining returnable quantity under lock — the double-
    // booking guard. sumOpenReturnedQtyForLine counts this same pending
    // return too (it only excludes 'rejected'/'reverted'), so it's
    // subtracted back out before comparing against what else is claimed on
    // the line.
    const totalOpen = await sumOpenReturnedQtyForLine(ret.order_product_id, conn);
    const otherOpen = totalOpen - ret.returned_quantity;
    const remaining = (line.dispatched_quantity ?? 0) - otherOpen;
    if (ret.returned_quantity > remaining) {
      throw new ConflictError(`Cannot approve — only ${remaining} unit(s) remain returnable on this line`);
    }

    const stages = getStages();
    const stageIndex = stages.findIndex((s) => s.id === ret.target_stage_id);
    if (stageIndex === -1) throw new ConflictError('Target stage no longer exists');
    const completedStages = stageIndex + 1;

    const dispatchableQty = line.quantity - (line.cancelled_quantity ?? 0);
    const newDispatchedQty = (line.dispatched_quantity ?? 0) - ret.returned_quantity;
    const newDispatchedWeight = (line.dispatched_weight ?? 0) - ret.returned_weight;
    const fullyDispatched = newDispatchedQty >= dispatchableQty;

    // Same close-current-stage-then-insert-new-row mechanism advanceStage()
    // uses in order.service.ts, so the line's stage history stays standard.
    const prevStage = await getCurrentStage(line.id, conn);
    if (prevStage) {
      await completeStage(line.id, prevStage.stage_id, now(), conn);
    }
    // The "[RETURN] " prefix is a private marker log.service.ts's
    // buildStageChangeRows() looks for to label this row as a distinct
    // "Returned" event on the Logs page instead of a generic stage change —
    // not shown to the user verbatim (the Logs page strips it before
    // displaying the reason).
    await createWithId('order_stage_history', 'STG', (historyId) => insertStageHistory(
      { order_product_id: line.id, stage_id: ret.target_stage_id, entered_at: now(), employee_id: reviewerId, remarks: `[RETURN] ${ret.reason}` },
      historyId, conn,
    ), conn);

    await applyReturn(line.id, {
      dispatched_quantity: newDispatchedQty,
      dispatched_weight: newDispatchedWeight,
      dispatched: fullyDispatched,
      return_id: ret.id,
      completed_stages: completedStages,
    }, reviewerId, conn);

    await markActive(ret.id, reviewerId, conn);

    await conn.commit();

    emitter.emit('activity.create', {
      entity_type: 'order_return', entity_id: ret.id, action: 'return_approved',
      new_values: { target_stage_id: ret.target_stage_id }, user_id: reviewerId,
    });

    return get(ret.id);
  } catch (err) {
    await conn.rollback();
    throw err;
  } finally {
    conn.release();
  }
}

export async function reject(returnId: string, reviewerId: string, reason: string): Promise<OrderReturn> {
  if (!reason?.trim()) throw new ConflictError('A rejection reason is required');

  const ret = await findById(returnId);
  if (!ret) throw new NotFoundError('Return not found');
  if (ret.status !== 'pending') throw new ConflictError('This return has already been reviewed');

  await markRejected(returnId, reviewerId, reason);

  emitter.emit('activity.create', {
    entity_type: 'order_return', entity_id: returnId, action: 'return_rejected',
    new_values: { rejection_reason: reason }, user_id: reviewerId,
  });

  return get(returnId);
}

// Undoes an approved-but-not-yet-fully-redelivered return, putting the line
// back to being a normal delivered line — the exact inverse of approve()'s
// dispatched-counter rollback. Only allowed while status is 'active': once
// 'completed' the return has already been fully redelivered through the
// ordinary dispatch flow and there's nothing left to undo; once 'rejected'
// or already 'reverted' there's nothing that was ever applied.
//
// Restores by the *lesser* of the return's own returned_quantity and
// whatever gap is still actually open on the line (dispatchable - currently
// dispatched) — if some of the returned quantity was already redelivered
// through the normal dispatch flow before this revert was requested, that
// portion stays delivered as-is (it went through the proper channel) and
// only the still-outstanding remainder is restored, so this can never push
// dispatched_quantity past the line's own dispatchable quantity.
export async function revert(returnId: string, reviewerId: string): Promise<OrderReturn> {
  const conn = await getConnection();
  try {
    await conn.beginTransaction();

    const ret = await findByIdForUpdate(returnId, conn);
    if (!ret) throw new NotFoundError('Return not found');
    if (ret.status !== 'active') throw new ConflictError('Only an approved, not-yet-fully-redelivered return can be reverted');

    const line = await findOrderProductForUpdate(ret.order_product_id, conn);
    if (!line) throw new NotFoundError('Original order line not found');

    const dispatchableQty = line.quantity - (line.cancelled_quantity ?? 0);
    const currentDispatchedQty = line.dispatched_quantity ?? 0;
    const currentDispatchedWeight = line.dispatched_weight ?? 0;

    const restoreQty = Math.min(ret.returned_quantity, dispatchableQty - currentDispatchedQty);
    if (restoreQty <= 0) {
      throw new ConflictError('This return has already been fully redelivered through normal dispatch — nothing left to revert');
    }
    const restoreWeight = ret.returned_quantity > 0 ? (ret.returned_weight * restoreQty) / ret.returned_quantity : 0;

    const newDispatchedQty = currentDispatchedQty + restoreQty;
    const newDispatchedWeight = currentDispatchedWeight + restoreWeight;
    const fullyDispatched = newDispatchedQty >= dispatchableQty;

    await applyRevert(line.id, {
      dispatched_quantity: newDispatchedQty,
      dispatched_weight: newDispatchedWeight,
      dispatched: fullyDispatched,
    }, reviewerId, conn);

    await markReverted(ret.id, reviewerId, conn);

    await conn.commit();

    emitter.emit('activity.create', {
      entity_type: 'order_return', entity_id: ret.id, action: 'return_reverted', user_id: reviewerId,
    });

    return get(ret.id);
  } catch (err) {
    await conn.rollback();
    throw err;
  } finally {
    conn.release();
  }
}

export interface UpdateReturnRequestData {
  returned_quantity: number;
  returned_weight: number;
  reason: string;
  target_stage_id: string;
  returned_date: string;
  next_delivery_date?: string | null;
}

// Lets the requester revise their own still-pending return (or an approver
// revise anyone's) — deliberately scoped to only the fields captured at
// request time (quantity/weight/reason/target stage/dates), never the
// order/line it targets or anything about the order itself. Only allowed
// while status is 'pending' — approval is final (see approve()'s own
// comment), so once reviewed the requester resubmits fresh rather than
// editing a decided request.
export async function updateRequest(
  returnId: string,
  data: UpdateReturnRequestData,
  actingEmployeeId: string,
  isApprover: boolean,
): Promise<OrderReturn> {
  if (data.returned_quantity <= 0) throw new ConflictError('Returned quantity must be greater than zero');
  if (data.returned_weight <= 0) throw new ConflictError('Returned weight must be greater than zero');
  if (!data.reason?.trim()) throw new ConflictError('A reason is required');

  const stage = getStages().find((s) => s.id === data.target_stage_id);
  if (!stage) throw new ConflictError('Invalid target stage');

  const conn = await getConnection();
  try {
    await conn.beginTransaction();

    const ret = await findByIdForUpdate(returnId, conn);
    if (!ret) throw new NotFoundError('Return not found');
    if (ret.status !== 'pending') throw new ConflictError('Only a pending return can be edited');
    if (!isApprover && ret.requested_by_employee_id !== actingEmployeeId) {
      throw new ConflictError('You can only edit your own return requests');
    }

    const line = await findOrderProductForUpdate(ret.order_product_id, conn);
    if (!line) throw new NotFoundError('Original order line not found');

    // Same double-booking guard as insertPendingReturn(), minus this
    // return's own existing claim (being replaced by the new figure).
    const totalOpen = await sumOpenReturnedQtyForLine(ret.order_product_id, conn);
    const otherOpen = totalOpen - ret.returned_quantity;
    const remaining = (line.dispatched_quantity ?? 0) - otherOpen;
    if (data.returned_quantity > remaining) {
      throw new ConflictError(`Cannot return ${data.returned_quantity} unit(s) — only ${remaining} unit(s) remain returnable on this line`);
    }

    await updateReturnRow(returnId, {
      returned_quantity: data.returned_quantity,
      returned_weight: data.returned_weight,
      reason: data.reason,
      target_stage_id: data.target_stage_id,
      returned_date: data.returned_date,
      next_delivery_date: data.next_delivery_date ?? null,
    }, actingEmployeeId, conn);

    await conn.commit();

    emitter.emit('activity.create', {
      entity_type: 'order_return', entity_id: returnId, action: 'return_updated', user_id: actingEmployeeId,
    });

    return get(returnId);
  } catch (err) {
    await conn.rollback();
    throw err;
  } finally {
    conn.release();
  }
}
