/**
 * Returns service orchestrator.
 *
 * Encapsulates the returns state machine in usable APIs:
 *   - openReturn          — create a return request
 *   - approveReturn       — vendor approves
 *   - denyReturn          — vendor denies (spawns support case)
 *   - enterTracking       — buyer enters carrier + tracking
 *   - confirmReceipt      — vendor confirms item received
 *   - inspectApprove      — vendor inspection passed → refund
 *   - inspectDispute      — vendor inspection failed (spawns support case)
 *   - cancelReturn        — buyer cancels
 *   - processSlaTick      — called by CF Cron to auto-advance overdue returns
 */

import { eq, and, lt, sql } from 'drizzle-orm';
import type { DrizzleClient, TxDrizzleClient } from '@/server/db/client';
import type { MultidealEnv } from '@/server/env';
import type { PaymentProvider } from '@/server/payments/provider';
import {
  returns,
  outbox,
  supportAttachments,
  orderLine,
  dealSkus,
  deals,
  vendors,
  order,
  shipmentPurchases,
  shipments,
  shipmentEvents,
  dealCategories,
} from '@/server/db/schema';
import { lineRedemptionStatusSql } from '@/server/fulfillment/voucher-line-state.js';
import { decide } from '@/server/domain/returns/machine';
import type { ReturnReason, Command, ReturnState } from '@/server/domain/returns/machine';
import { applyEvents, initialDeadlines } from '@/server/domain/returns/apply-effects';
import {
  eligibleToOpenReturn,
  validateReturnRequest,
  computeRefundBreakdown,
  computeHybridSplit,
  deriveLiability,
} from '@/server/returns/policy';
import { executeRefund, RefundError } from '@/server/workflows/refund';
import { formatAgorotPlain } from '@/lib/money';
import { openCase } from '@/server/support/workflows/case';
import { enqueueOutbox } from '@/server/queues/outbox-producer.js';
import { asCaseId } from '@/server/platform-seams/ids.js';
import { isDealCategoryStatutoryExempt } from '@/server/domain/refund/category-exemptions.js';

function coerceDeliveredAt(value: Date | string | number | null | undefined): Date | null {
  if (value == null) return null;
  const date = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(date.getTime())) return null;
  return date;
}

// ─── Deps ─────────────────────────────────────────────────────────────────────

export interface ReturnsServiceDeps {
  db: TxDrizzleClient;
  env: MultidealEnv;
  payments: () => Promise<PaymentProvider>;
}

// ─── Input types ──────────────────────────────────────────────────────────────

export type OpenReturnInput = {
  buyerId: string;
  purchaseId: string;
  reason: ReturnReason;
  reasonNote?: string;
  /** UUID of the upload session used for pre-uploaded photos. */
  uploadSessionId?: string;
  photoCount: number;
};

// ─── Errors ───────────────────────────────────────────────────────────────────

export class ReturnsServiceError extends Error {
  readonly code: string;
  constructor(code: string, message: string) {
    super(message);
    this.name = 'ReturnsServiceError';
    this.code = code;
  }
}

// ─── openReturn ───────────────────────────────────────────────────────────────

function isUniqueViolation(e: unknown): boolean {
  const err = e as {
    code?: string;
    constraint?: string;
    constraint_name?: string;
    cause?: { code?: string; constraint?: string; constraint_name?: string };
  } | null;
  const code = err?.code ?? err?.cause?.code;
  if (code !== '23505') return false;
  const name =
    err?.constraint ??
    err?.cause?.constraint ??
    err?.constraint_name ??
    err?.cause?.constraint_name;
  return name == null || name === 'returns_purchase_open_uniq';
}

export async function openReturn(
  deps: ReturnsServiceDeps,
  input: OpenReturnInput,
): Promise<{ returnId: string }> {
  const now = new Date();
  const db = deps.db as unknown as DrizzleClient;

  const [row] = await db
    .select({
      id: orderLine.id,
      buyerUserId: order.buyerUserId,
      dealId: dealSkus.dealId,
      vendorId: orderLine.vendorId,
      lineTotal: orderLine.lineTotal,
      orderStatus: order.status,
      redemptionStatus: lineRedemptionStatusSql(orderLine.id),
      dealIdJoined: deals.id,
      categorySlug: dealCategories.slug,
      isPhysical: deals.isPhysical,
      isReturnable: deals.isReturnable,
      returnWindowDays: deals.returnWindowDays,
      restockingFeePct: vendors.restockingFeePct,
      deliveredAt: sql<Date | null>`(
        SELECT COALESCE(s.delivered_at, MAX(se.created_at))
        FROM ${shipmentPurchases} sp
        INNER JOIN ${shipments} s ON s.id = sp.shipment_id
        LEFT JOIN ${shipmentEvents} se
          ON se.shipment_id = s.id
         AND se.to_status = 'delivered'
        WHERE sp.order_line_id = ${orderLine.id}
          AND s.status = 'delivered'
        GROUP BY s.id, s.delivered_at
        LIMIT 1
      )`,
      openReturnExists: sql<boolean>`EXISTS (SELECT 1 FROM ${returns} r WHERE r.order_line_id = ${orderLine.id} AND r.status IN ('requested','approved','in_transit','received'))`,
    })
    .from(orderLine)
    .innerJoin(order, eq(order.id, orderLine.orderId))
    .innerJoin(dealSkus, eq(dealSkus.id, orderLine.variantId))
    .leftJoin(deals, eq(deals.id, dealSkus.dealId))
    .leftJoin(dealCategories, eq(dealCategories.id, deals.categoryId))
    .leftJoin(vendors, eq(sql`${vendors.id}::text`, orderLine.vendorId))
    .where(eq(orderLine.id, input.purchaseId));

  if (!row) {
    throw new ReturnsServiceError('purchase_not_found', `Order line ${input.purchaseId} not found`);
  }
  if (row.buyerUserId !== input.buyerId) {
    // Same code/message as the missing-row branch: a non-owner must not learn the ID exists.
    throw new ReturnsServiceError('purchase_not_found', `Order line ${input.purchaseId} not found`);
  }
  if (!row.dealIdJoined) {
    throw new ReturnsServiceError(
      'deal_not_found',
      `Deal for order line ${input.purchaseId} not found`,
    );
  }

  const eligibility = eligibleToOpenReturn({
    purchase: {
      paymentStatus: row.orderStatus === 'completed' ? 'COMPLETED' : row.orderStatus.toUpperCase(),
      redemptionStatus: row.redemptionStatus ?? 'UNREDEEMED',
      deliveredAt: coerceDeliveredAt(row.deliveredAt),
    },
    deal: {
      isPhysical: row.isPhysical!,
      isReturnable: row.isReturnable!,
      returnWindowDays: row.returnWindowDays!,
    },
    openReturnExists: row.openReturnExists,
    now,
  });
  if (!eligibility.ok) {
    throw new ReturnsServiceError(
      `not_eligible:${eligibility.reason}`,
      `Not eligible: ${eligibility.reason}`,
    );
  }

  if (isDealCategoryStatutoryExempt(row.categorySlug)) {
    throw new ReturnsServiceError(
      'not_eligible:statutory_exempt',
      'Not eligible: statutory_exempt',
    );
  }

  const validation = validateReturnRequest({
    reason: input.reason,
    photoCount: input.photoCount,
  });
  if (!validation.ok) {
    throw new ReturnsServiceError(
      `invalid:${validation.reason}`,
      `Invalid request: ${validation.reason}`,
    );
  }

  // lineTotal is stored as bigint agorot
  const amountPaidAgorot = Number(row.lineTotal);
  const vendorRestockingPct = row.restockingFeePct != null ? Number(row.restockingFeePct) : 0;
  const breakdown = computeRefundBreakdown({
    amountPaidAgorot,
    reason: input.reason,
    vendorRestockingPct,
    returnShippingCostAgorot: 0, // Buyer provides shipping cost separately if needed
  });

  const { cashRefundAgorot, creditOverflowAgorot } = computeHybridSplit({
    amountPaidAgorot,
    buyerRefundAgorot: breakdown.refundAmountAgorot - breakdown.restockingFeeAgorot,
  });

  const deadlines = initialDeadlines(now);

  try {
    const createdId = await deps.db.transaction(async (tx) => {
      const txDb = tx as unknown as DrizzleClient;

      const [created] = await txDb
        .insert(returns)
        .values({
          orderLineId: input.purchaseId,
          buyerId: input.buyerId,
          vendorId: row.vendorId ?? '',
          dealId: row.dealId,
          status: 'requested',
          reason: input.reason,
          reasonNote: input.reasonNote ?? null,
          liability: deriveLiability(input.reason),
          refundAmountAgorot: breakdown.refundAmountAgorot,
          restockingFeeAgorot: breakdown.restockingFeeAgorot,
          shippingReimbursementAgorot: breakdown.shippingReimbursementAgorot,
          cashRefundAgorot,
          creditOverflowAgorot,
          vendorRespondBy: deadlines.vendorRespondBy,
          metadata: {},
        })
        .returning({ id: returns.id });

      if (!created) {
        throw new ReturnsServiceError('insert_failed', 'Failed to create return');
      }

      if (input.uploadSessionId && input.photoCount > 0) {
        await txDb
          .update(supportAttachments)
          .set({ parentType: 'return', parentId: created.id })
          .where(
            and(
              eq(supportAttachments.parentType, 'pending_return'),
              eq(supportAttachments.parentId, input.uploadSessionId),
              eq(supportAttachments.uploaderId, input.buyerId),
            ),
          );
      }

      await txDb.insert(outbox).values({
        aggregateType: 'return',
        aggregateId: created.id,
        eventType: 'returns.opened',
        payload: { returnId: created.id },
      });

      return created.id;
    });

    return { returnId: createdId };
  } catch (e) {
    if (isUniqueViolation(e)) {
      throw new ReturnsServiceError(
        'not_eligible:open_return_exists',
        'Not eligible: open_return_exists',
      );
    }
    throw e;
  }
}

// ─── Internal transition helper ───────────────────────────────────────────────

const returnRowSelect = {
  id: returns.id,
  status: returns.status,
  reason: returns.reason,
  liability: returns.liability,
  refundAmountAgorot: returns.refundAmountAgorot,
  restockingFeeAgorot: returns.restockingFeeAgorot,
  shippingReimbursementAgorot: returns.shippingReimbursementAgorot,
  orderLineId: returns.orderLineId,
  buyerId: returns.buyerId,
  vendorId: returns.vendorId,
} as const;

type ReturnRow = {
  id: string;
  status: ReturnState['status'];
  reason: ReturnReason;
  liability: ReturnState['liability'];
  refundAmountAgorot: number;
  restockingFeeAgorot: number;
  shippingReimbursementAgorot: number;
  orderLineId: string;
  buyerId: string;
  vendorId: string;
};

function assertReturnActorOwnership(r: ReturnRow, cmd: Command): void {
  // Commands prefixed 'buyer.' must be issued by the return's buyer.
  // Commands prefixed 'vendor.' must be issued by the return's vendor.
  // Commands prefixed 'sla.' and 'admin.' carry no buyer/vendor actorId — skip.
  if ('actorId' in cmd) {
    if (cmd.kind.startsWith('buyer.') && cmd.actorId !== r.buyerId) {
      throw new ReturnsServiceError('forbidden', 'Actor is not the buyer for this return');
    }
    if (cmd.kind.startsWith('vendor.') && cmd.actorId !== r.vendorId) {
      throw new ReturnsServiceError('forbidden', 'Actor is not the vendor for this return');
    }
  }
}

function toReturnState(r: ReturnRow): ReturnState {
  return {
    status: r.status,
    reason: r.reason,
    liability: r.liability,
    refundAmountAgorot: r.refundAmountAgorot,
    restockingFeeAgorot: r.restockingFeeAgorot,
    shippingReimbursementAgorot: r.shippingReimbursementAgorot,
  };
}

async function applyDeniedSideEffects(
  txDb: TxDrizzleClient,
  deps: ReturnsServiceDeps,
  r: ReturnRow,
  cmd: Command,
  events: ReturnType<typeof decide>,
  now: Date,
  returnId: string,
): Promise<void> {
  for (const ev of events) {
    if (ev.kind === 'denied' && cmd.kind === 'vendor.deny') {
      const denialReason =
        'reason' in cmd ? (cmd as { kind: string; reason: string }).reason : 'unspecified';
      const caseResult = await openCase(
        {
          db: txDb,
          messageEncryptionKey: deps.env.PII_KEY,
          env: deps.env,
        },
        {
          orderLineId: r.orderLineId,
          customerId: r.buyerId,
          vendorId: r.vendorId,
          category: 'item_not_as_described',
          customerAskOutcome: 'refund_full',
          customerAskAmountCents: r.refundAmountAgorot,
          description: `Return denied: ${denialReason}`,
          escapeRequested: false,
        },
      );

      await txDb
        .update(returns)
        .set({ caseId: asCaseId(caseResult.caseId), updatedAt: now })
        .where(eq(returns.id, returnId));
    }

    if (ev.kind === 'denied' && cmd.kind === 'vendor.inspect_dispute') {
      const disputeReason =
        'reason' in cmd ? (cmd as { kind: string; reason: string }).reason : 'unspecified';
      const caseResult = await openCase(
        {
          db: txDb,
          messageEncryptionKey: deps.env.PII_KEY,
          env: deps.env,
        },
        {
          orderLineId: r.orderLineId,
          customerId: r.buyerId,
          vendorId: r.vendorId,
          category: 'item_not_as_described',
          customerAskOutcome: 'refund_full',
          customerAskAmountCents: r.refundAmountAgorot,
          description: `Return denied after inspection: ${disputeReason}`,
          escapeRequested: false,
        },
      );

      await txDb
        .update(returns)
        .set({ caseId: asCaseId(caseResult.caseId), updatedAt: now })
        .where(eq(returns.id, returnId));
    }
  }
}

async function transition(deps: ReturnsServiceDeps, returnId: string, cmd: Command): Promise<void> {
  const now = new Date();

  const [r] = await deps.db.select(returnRowSelect).from(returns).where(eq(returns.id, returnId));

  if (!r) {
    throw new ReturnsServiceError('return_not_found', `Return ${returnId} not found`);
  }

  assertReturnActorOwnership(r, cmd);
  const events = decide(toReturnState(r), cmd);
  const hasRefundExecuted = events.some((ev) => ev.kind === 'refund_executed');

  if (!hasRefundExecuted) {
    await deps.db.transaction(async (tx) => {
      const txDb = tx as unknown as DrizzleClient;

      const [txR] = await txDb
        .select(returnRowSelect)
        .from(returns)
        .where(eq(returns.id, returnId));

      if (!txR) {
        throw new ReturnsServiceError('return_not_found', `Return ${returnId} not found`);
      }

      assertReturnActorOwnership(txR, cmd);
      const txEvents = decide(toReturnState(txR), cmd);

      await applyEvents({ tx: txDb, returnId, now }, txEvents);
      await applyDeniedSideEffects(tx, deps, txR, cmd, txEvents, now, returnId);
    });
    return;
  }

  const expectedStatus = r.status;
  const { cashRefundAgorot } = computeHybridSplit({
    amountPaidAgorot: r.refundAmountAgorot,
    buyerRefundAgorot: r.refundAmountAgorot - r.restockingFeeAgorot,
  });
  const creditOverflowAgorot = Math.max(
    0,
    r.refundAmountAgorot - r.restockingFeeAgorot - cashRefundAgorot,
  );

  let providerRefundId: string | undefined;
  let refundOutboxId: string | undefined;

  if (cashRefundAgorot > 0) {
    // Resolve order data from orderLine (purchase.id → orderLine.id in new model)
    const [lineWithOrder] = await deps.db
      .select({
        lineTotal: orderLine.lineTotal,
        chargeRef: order.chargeRef,
        orderStatus: order.status,
        orderId: order.id,
      })
      .from(orderLine)
      .innerJoin(order, eq(order.id, orderLine.orderId))
      .where(eq(orderLine.id, r.orderLineId))
      .limit(1);

    if (!lineWithOrder) {
      throw new ReturnsServiceError('purchase_not_found', `Order line ${r.orderLineId} not found`);
    }

    const refundAmountPaidAgorot = Number(lineWithOrder.lineTotal);

    if (lineWithOrder.orderStatus === 'refunded') {
      // Already refunded — providerRefundId not available in new schema; executeRefund will handle idempotency
    } else {
      const safeCashAgorot = Math.min(cashRefundAgorot, refundAmountPaidAgorot);
      const amountIls = formatAgorotPlain(safeCashAgorot);

      try {
        const result = await executeRefund(
          { db: deps.db, payments: deps.payments },
          {
            purchaseId: r.orderLineId,
            amount: amountIls,
            refundEventId: `return:${returnId}`,
          },
        );
        providerRefundId = result.refundId;
        if (result.outboxId) refundOutboxId = result.outboxId;
      } catch (err) {
        if (err instanceof RefundError && err.code === 'INVALID_STATE') {
          throw err;
        } else {
          throw err;
        }
      }
    }
  }

  await deps.db.transaction(async (tx) => {
    const txDb = tx as unknown as DrizzleClient;

    const [current] = await txDb
      .select({ status: returns.status })
      .from(returns)
      .where(eq(returns.id, returnId));

    if (!current) {
      throw new ReturnsServiceError('return_not_found', `Return ${returnId} not found`);
    }
    if (current.status !== expectedStatus) {
      throw new ReturnsServiceError(
        'conflict',
        `Return status changed from ${expectedStatus} to ${current.status}`,
      );
    }

    await applyEvents({ tx: txDb, returnId, now }, events);

    await txDb
      .update(returns)
      .set({
        cashRefundAgorot,
        creditOverflowAgorot,
        ...(providerRefundId ? { providerRefundId } : {}),
        updatedAt: now,
      })
      .where(eq(returns.id, returnId));

    await applyDeniedSideEffects(tx, deps, r, cmd, events, now, returnId);

    // Refund bookkeeping (refund_intent row + order status) is owned by
    // executeRefund above — the claim records the intent, the provider settles
    // it, and recomputeOrderRefundStatus derives refunded/partially_refunded.
    // A second insert here double-counts the refund against the order cap.
  });

  if (refundOutboxId) {
    await enqueueOutbox(refundOutboxId);
  }
}

// ─── Public wrappers ──────────────────────────────────────────────────────────

export async function approveReturn(
  deps: ReturnsServiceDeps,
  args: { returnId: string; vendorActorId: string },
): Promise<void> {
  await transition(deps, args.returnId, {
    kind: 'vendor.approve',
    actorId: args.vendorActorId,
  });
}

export async function denyReturn(
  deps: ReturnsServiceDeps,
  args: { returnId: string; vendorActorId: string; reason: string },
): Promise<void> {
  await transition(deps, args.returnId, {
    kind: 'vendor.deny',
    actorId: args.vendorActorId,
    reason: args.reason,
  });
}

export async function enterTracking(
  deps: ReturnsServiceDeps,
  args: {
    returnId: string;
    buyerActorId: string;
    carrier: string;
    trackingNumber: string;
  },
): Promise<void> {
  await transition(deps, args.returnId, {
    kind: 'buyer.enter_tracking',
    actorId: args.buyerActorId,
    carrier: args.carrier,
    tracking: args.trackingNumber,
  });
}

export async function confirmReceipt(
  deps: ReturnsServiceDeps,
  args: { returnId: string; vendorActorId: string },
): Promise<void> {
  await transition(deps, args.returnId, {
    kind: 'vendor.confirm_receipt',
    actorId: args.vendorActorId,
  });
}

export async function inspectApprove(
  deps: ReturnsServiceDeps,
  args: { returnId: string; vendorActorId: string },
): Promise<void> {
  await transition(deps, args.returnId, {
    kind: 'vendor.inspect_approve',
    actorId: args.vendorActorId,
  });
}

export async function inspectDispute(
  deps: ReturnsServiceDeps,
  args: { returnId: string; vendorActorId: string; reason: string },
): Promise<void> {
  await transition(deps, args.returnId, {
    kind: 'vendor.inspect_dispute',
    actorId: args.vendorActorId,
    reason: args.reason,
  });
}

export async function cancelReturn(
  deps: ReturnsServiceDeps,
  args: { returnId: string; buyerActorId: string },
): Promise<void> {
  await transition(deps, args.returnId, {
    kind: 'buyer.cancel',
    actorId: args.buyerActorId,
  });
}

// ─── processSlaTick ───────────────────────────────────────────────────────────

/**
 * Called by CF Cron. Advances overdue returns through SLA timeouts.
 * Each overdue return is processed in its own transaction.
 */
export async function processSlaTick(deps: ReturnsServiceDeps): Promise<void> {
  const now = new Date();

  // Overdue vendor response: requested → auto-approve
  const overdueRespond = await deps.db
    .select({ id: returns.id })
    .from(returns)
    .where(and(eq(returns.status, 'requested'), lt(returns.vendorRespondBy, now)));

  for (const row of overdueRespond) {
    await transition(deps, row.id, { kind: 'sla.vendor_respond_timeout' });
  }

  // Overdue buyer ship: approved → expire
  const overdueShip = await deps.db
    .select({ id: returns.id })
    .from(returns)
    .where(and(eq(returns.status, 'approved'), lt(returns.buyerShipBy, now)));

  for (const row of overdueShip) {
    await transition(deps, row.id, { kind: 'sla.buyer_ship_timeout' });
  }

  // Overdue vendor inspection: received → auto-refund
  const overdueInspect = await deps.db
    .select({ id: returns.id })
    .from(returns)
    .where(and(eq(returns.status, 'received'), lt(returns.vendorInspectBy, now)));

  for (const row of overdueInspect) {
    await transition(deps, row.id, { kind: 'sla.vendor_inspect_timeout' });
  }
}

// ─── Admin overrides ──────────────────────────────────────────────────────────

export async function adminOverrideRefund(
  deps: ReturnsServiceDeps,
  args: { returnId: string; adminActorId: string; reason: string },
): Promise<void> {
  await transition(deps, args.returnId, {
    kind: 'admin.override_refund',
    actorId: args.adminActorId,
    reason: args.reason,
  });
}

export async function adminOverrideClose(
  deps: ReturnsServiceDeps,
  args: { returnId: string; adminActorId: string; reason: string },
): Promise<void> {
  await transition(deps, args.returnId, {
    kind: 'admin.override_close',
    actorId: args.adminActorId,
    reason: args.reason,
  });
}
