import { Order, OrderComment, ReturnRequest } from "@/models";
import {
  buildCommentAudienceFilter,
  NOT_DELETED_ORDER_COMMENT_FILTER,
} from "@/models/order-comment.model";
import { AuditLog } from "@/models/audit-log.model";
import { connectDB } from "@/lib/db";
import { isValidObjectId } from "@/lib/api/validate";
import {
  buildStaffOrderScopeFilter,
  mergeScopeFilter,
  type StaffAccessScope,
} from "@/lib/staff-scope";

/**
 * Server-side loaders for the order details screen (admin and staff).
 *
 * These replace the two client mount fetches the page used to fire — one to
 * `/api/admin/returns`, one to `/api/admin/audit-logs` — which could only
 * start after the HTML shipped and the bundle hydrated. Run here they overlap
 * with the order query instead of queueing behind it, and each returns only
 * the handful of fields its component renders rather than whole documents.
 *
 * Every loader takes the caller's staff scope and applies the SAME filter the
 * `/api/admin/orders/[id]` routes apply. Without it a scoped staff member (one
 * limited to a vendor, POS location or fulfillment region) is filtered out of
 * the orders list yet can still open any order — with its customer PII — by
 * typing the id into the URL.
 */

export interface OrderReturnRequestSummary {
  _id: string;
  returnNumber: string;
  status: string;
  refundStatus?: string;
  /** Value of the returned goods; caps what may be refunded via this return. */
  estimatedRefundTotal: number;
  /** Already refunded against this return, so the remaining cap is derivable. */
  actualRefundAmount: number;
}

/**
 * One row in the merged timeline.
 *
 * `kind` is the discriminant between the two backing stores: system events
 * come from `AuditLog`, comments from `OrderComment`. They are merged here
 * rather than in the component so the card stays a dumb renderer, and merged
 * at read time rather than by writing comments into the audit log — see the
 * header of `models/order-comment.model.ts` for why that separation matters.
 */
export interface OrderTimelineEntry {
  _id: string;
  kind: "event" | "comment";
  createdAt: string;

  /** Event rows only. */
  action?: string;
  userEmail?: string;
  summary?: string;
  /**
   * Resulting status, for status-change rows only. A cancellation is a
   * STATUS_CHANGE like any other, so without this the card gave it the same
   * green check as "shipped" — the one transition that must not look like good
   * news.
   */
  status?: string;

  /** Comment rows only. */
  body?: string;
  authorId?: string;
  authorName?: string;
  authorImage?: string;
  editedAt?: string;
}

const TIMELINE_LIMIT = 50;
const COMMENT_LIMIT = 50;
const RETURN_REQUEST_LIMIT = 20;

export async function getOrderDetails(
  id: string,
  staffScope?: StaffAccessScope | null,
) {
  // An id that is not an ObjectId used to reach `findById` and throw a
  // CastError, so a mistyped URL rendered the 500 page instead of 404.
  if (!isValidObjectId(id)) return null;

  await connectDB();
  const order = await Order.findOne(
    mergeScopeFilter({ _id: id }, buildStaffOrderScopeFilter(staffScope)),
  )
    // `createdAt` backs the "customer since" line — without it the panel has
    // no signup date to show.
    .populate("customerId", "name email phone image createdAt")
    .lean();

  if (!order) return null;

  // ObjectIds and Dates are not serializable across the RSC boundary.
  return JSON.parse(JSON.stringify(order));
}

export async function getOrderReturnRequests(
  orderId: string,
  staffScope?: StaffAccessScope | null,
): Promise<OrderReturnRequestSummary[]> {
  if (!isValidObjectId(orderId)) return [];

  await connectDB();
  // Return requests carry the same customer data as the order, so they are
  // gated on the caller being able to see the order in the first place.
  const order = await Order.findOne(
    mergeScopeFilter({ _id: orderId }, buildStaffOrderScopeFilter(staffScope)),
  )
    .select("_id")
    .lean();
  if (!order) return [];

  const requests = await ReturnRequest.find({
    orderId,
    // Same ownership scope the admin returns API applies, so vendor-owned
    // requests stay out of the admin/staff view.
    $or: [{ ownerType: "admin" }, { ownerType: { $exists: false } }],
  })
    .select("returnNumber status refundStatus estimatedRefund.total actualRefund.amount")
    .sort({ createdAt: -1 })
    .limit(RETURN_REQUEST_LIMIT)
    .lean();

  return requests.map((request) => ({
    _id: String(request._id),
    returnNumber: String(request.returnNumber || ""),
    status: String(request.status || ""),
    refundStatus: request.refundStatus
      ? String(request.refundStatus)
      : undefined,
    estimatedRefundTotal: Number(request.estimatedRefund?.total || 0),
    actualRefundAmount: Number(request.actualRefund?.amount || 0),
  }));
}

export async function getOrderTimeline(
  orderId: string,
  staffScope?: StaffAccessScope | null,
): Promise<OrderTimelineEntry[]> {
  if (!isValidObjectId(orderId)) return [];

  await connectDB();
  // Scoped like its siblings. Both pages already 404 out-of-scope orders via
  // `getOrderDetails`, but this loader is exported and must not rely on a
  // caller having done that first.
  const order = await Order.findOne(
    mergeScopeFilter({ _id: orderId }, buildStaffOrderScopeFilter(staffScope)),
  )
    .select("_id")
    .lean();
  if (!order) return [];

  const [logs, comments] = await Promise.all([
    // Served by the {resource, resourceId, createdAt} index. Audit entries
    // carry full before/after diffs and metadata blobs; the card shows none of
    // it.
    AuditLog.find({ resource: "order", resourceId: orderId })
      .select("action createdAt userEmail changes.summary changes.after.status")
      .sort({ createdAt: -1 })
      .limit(TIMELINE_LIMIT)
      .lean(),
    // Served by the {orderId, createdAt} index. Author fields are denormalized
    // on the comment, so no populate.
    OrderComment.find({
      orderId,
      ...NOT_DELETED_ORDER_COMMENT_FILTER,
      ...buildCommentAudienceFilter(staffScope?.vendorIds),
    })
      .select("body authorId authorName authorImage editedAt createdAt")
      .sort({ createdAt: -1 })
      .limit(COMMENT_LIMIT)
      .lean(),
  ]);

  const events: OrderTimelineEntry[] = logs.map((log) => ({
    _id: String(log._id),
    kind: "event",
    action: String(log.action || ""),
    createdAt: new Date(log.createdAt).toISOString(),
    userEmail: log.userEmail || undefined,
    summary: log.changes?.summary || undefined,
    // Only status-change rows carry a resulting status. `auditUpdate` stores
    // the WHOLE updated order as `changes.after`, so an ordinary edit to an
    // already-cancelled order would otherwise report status "cancelled" and be
    // painted with the red cancellation icon.
    status:
      log.action === "STATUS_CHANGE"
        ? (log.changes?.after as { status?: string } | undefined)?.status
        : undefined,
  }));

  const notes: OrderTimelineEntry[] = comments.map((comment) => ({
    _id: String(comment._id),
    kind: "comment",
    createdAt: new Date(comment.createdAt).toISOString(),
    body: String(comment.body || ""),
    authorId: String(comment.authorId || ""),
    authorName: String(comment.authorName || ""),
    authorImage: comment.authorImage || undefined,
    editedAt: comment.editedAt
      ? new Date(comment.editedAt).toISOString()
      : undefined,
  }));

  // Newest first, matching the composer sitting at the top of the card.
  return [...events, ...notes].sort((a, b) =>
    b.createdAt.localeCompare(a.createdAt),
  );
}
