export type SalesOrderLifecycleStatus =
  | "DRAFT"
  | "SUBMITTED"
  | "CONFIRMED"
  | "CREDIT_HOLD"
  | "PARTIALLY_SHIPPED"
  | "SHIPPED"
  | "PARTIALLY_INVOICED"
  | "INVOICED"
  | "CANCELLED"
  | "CLOSED";

export interface PaymentTermScheduleLineInput {
  kind?: "NET_DAYS" | "END_OF_MONTH" | "FIXED_DAY" | "INSTALLMENT";
  days?: number;
  fixedDayOfMonth?: number;
  percentage?: number;
  fixedAmount?: number;
  discountPercentage?: number;
  discountDays?: number;
  discountBasis?: "GROSS_AMOUNT" | "NET_AMOUNT";
  note?: string;
}

export interface PaymentTermSnapshot {
  id?: string;
  name: string;
  lines: PaymentTermScheduleLineInput[];
}

export interface SalesOrderLineInput {
  itemId: string;
  description?: string | null;
  quantity: number;
  unitPrice: number;
  currencyId: string;
  unitId?: string | null;
  matchedPriceRuleId?: string | null;
  billingPolicy?: "ORDER" | "SHIPMENT";
  requestedShipDate?: Date | null;
  requiresPhysicalFulfillment?: boolean;
  priceSnapshot?: Record<string, unknown> | null;
  shippedQuantity?: number;
  invoicedQuantity?: number;
}

export interface ShipmentLineInput {
  salesOrderLineId: string;
  quantity: number;
  unitId?: string | null;
  trackingReference?: string | null;
}

export interface SalesInvoiceLineInput {
  salesOrderLineId: string;
  shipmentLineId?: string | null;
  quantity: number;
  unitPrice?: number | null;
  taxCode?: string | null;
  taxableAmount?: number | null;
  taxAmount?: number | null;
  billingPolicy?: "ORDER" | "SHIPMENT";
}

export interface SalesCreditNoteLineInput {
  salesInvoiceLineId: string;
  quantity?: number | null;
  amount?: number | null;
  reason?: string | null;
  taxAmount?: number | null;
}

export interface DueScheduleLine {
  dueDate: Date;
  amount: number;
  discountDueDate?: Date | null;
  discountAmount?: number | null;
}

export interface PriceSuggestion {
  ruleId: string;
  priceListId: string;
  unitPrice: number;
}

export function asDate(value: Date | string | null | undefined): Date | null {
  if (!value) return null;
  return value instanceof Date ? value : new Date(value);
}

export function decodeJson<T>(value: unknown, fallback: T): T {
  if (value === null || value === undefined) {
    return fallback;
  }
  if (typeof value === "string") {
    return JSON.parse(value) as T;
  }
  return value as T;
}

export function encodeJson(value: unknown): string {
  return JSON.stringify(value);
}

export function sameCompanyScope(
  record: { companyId?: string | null } | null | undefined,
  companyId: string,
) {
  if (!record || !("companyId" in record)) return true;
  return (
    record.companyId === null || record.companyId === undefined || record.companyId === companyId
  );
}

export function effectiveDateInRange(
  date: Date,
  effectiveFrom?: Date | string | null,
  effectiveTo?: Date | string | null,
) {
  const from = asDate(effectiveFrom);
  const to = asDate(effectiveTo);
  if (from && date < from) return false;
  if (to && date > to) return false;
  return true;
}

export function summarizeOrderProgress(
  lines: {
    quantity: number;
    shippedQuantity?: number | null;
    invoicedQuantity?: number | null;
  }[],
) {
  let totalOrdered = 0;
  let totalShipped = 0;
  let totalInvoiced = 0;

  for (const line of lines) {
    totalOrdered += line.quantity;
    totalShipped += line.shippedQuantity ?? 0;
    totalInvoiced += line.invoicedQuantity ?? 0;
  }

  return {
    totalOrdered,
    totalShipped,
    totalInvoiced,
    openToShip: Math.max(totalOrdered - totalShipped, 0),
    openToInvoice: Math.max(totalOrdered - totalInvoiced, 0),
  };
}

export function deriveSalesOrderStatus(
  currentStatus: string,
  lines: {
    quantity: number;
    shippedQuantity?: number | null;
    invoicedQuantity?: number | null;
  }[],
): SalesOrderLifecycleStatus {
  if (
    currentStatus === "CANCELLED" ||
    currentStatus === "CLOSED" ||
    currentStatus === "CREDIT_HOLD"
  ) {
    return currentStatus as SalesOrderLifecycleStatus;
  }

  const { totalOrdered, totalShipped, totalInvoiced } = summarizeOrderProgress(lines);
  if (totalOrdered === 0) {
    return currentStatus as SalesOrderLifecycleStatus;
  }
  if (totalInvoiced >= totalOrdered) {
    return "INVOICED";
  }
  if (totalInvoiced > 0) {
    return "PARTIALLY_INVOICED";
  }
  if (totalShipped >= totalOrdered) {
    return "SHIPPED";
  }
  if (totalShipped > 0) {
    return "PARTIALLY_SHIPPED";
  }
  return currentStatus as SalesOrderLifecycleStatus;
}

export function validateScheduleLines(lines: PaymentTermScheduleLineInput[]) {
  if (lines.length === 0) return "EMPTY_SCHEDULE";

  let percentageTotal = 0;
  let maxDueDays = 0;

  for (const line of lines) {
    if ((line.percentage ?? 0) < 0 || (line.fixedAmount ?? 0) < 0) {
      return "INVALID_SCHEDULE_LINE";
    }
    if (line.percentage !== undefined) {
      percentageTotal += line.percentage;
    }
    maxDueDays = Math.max(maxDueDays, line.days ?? 0);
    if (line.discountPercentage !== undefined) {
      if (!line.discountBasis || line.discountDays === undefined) {
        return "INVALID_DISCOUNT_CONFIGURATION";
      }
      if (line.discountDays > (line.days ?? maxDueDays)) {
        return "INVALID_DISCOUNT_CONFIGURATION";
      }
    }
  }

  const hasInstallments = lines.some((line) => line.percentage !== undefined);
  if (hasInstallments && Math.abs(percentageTotal - 100) > 0.0001) {
    return "INVALID_INSTALLMENT_TOTAL";
  }

  return null;
}

function endOfMonth(date: Date) {
  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0));
}

function fixedDay(date: Date, day: number) {
  const year = date.getUTCFullYear();
  const month = date.getUTCMonth();
  const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  return new Date(Date.UTC(year, month, Math.min(day, lastDay)));
}

export function calculateDueSchedule(
  scheduleLines: PaymentTermScheduleLineInput[],
  invoiceDate: Date,
  invoiceAmount: number,
): DueScheduleLine[] | null {
  const scheduleError = validateScheduleLines(scheduleLines);
  if (scheduleError) return null;

  const dueLines: DueScheduleLine[] = [];
  let runningAllocated = 0;

  for (const [index, line] of scheduleLines.entries()) {
    const baseAmount =
      line.fixedAmount ??
      ((line.percentage ?? (index === scheduleLines.length - 1 ? 100 : 0)) / 100) * invoiceAmount;

    const roundedAmount =
      index === scheduleLines.length - 1
        ? Number((invoiceAmount - runningAllocated).toFixed(2))
        : Number(baseAmount.toFixed(2));
    runningAllocated += roundedAmount;

    const dueDate =
      line.kind === "END_OF_MONTH"
        ? endOfMonth(
            new Date(
              Date.UTC(
                invoiceDate.getUTCFullYear(),
                invoiceDate.getUTCMonth(),
                invoiceDate.getUTCDate() + (line.days ?? 0),
              ),
            ),
          )
        : line.kind === "FIXED_DAY" && line.fixedDayOfMonth
          ? fixedDay(
              new Date(
                Date.UTC(
                  invoiceDate.getUTCFullYear(),
                  invoiceDate.getUTCMonth(),
                  invoiceDate.getUTCDate() + (line.days ?? 0),
                ),
              ),
              line.fixedDayOfMonth,
            )
          : new Date(
              Date.UTC(
                invoiceDate.getUTCFullYear(),
                invoiceDate.getUTCMonth(),
                invoiceDate.getUTCDate() + (line.days ?? 0),
              ),
            );

    let discountDueDate: Date | null = null;
    let discountAmount: number | null = null;
    if (line.discountPercentage !== undefined && line.discountDays !== undefined) {
      discountDueDate = new Date(
        Date.UTC(
          invoiceDate.getUTCFullYear(),
          invoiceDate.getUTCMonth(),
          invoiceDate.getUTCDate() + line.discountDays,
        ),
      );
      discountAmount = Number((roundedAmount * (line.discountPercentage / 100)).toFixed(2));
      if (discountDueDate > dueDate) {
        return null;
      }
    }

    dueLines.push({
      dueDate,
      amount: roundedAmount,
      discountDueDate,
      discountAmount,
    });
  }

  return dueLines;
}

export function buildPaymentTermSnapshot(term: {
  id?: string;
  name: string;
  scheduleLines: unknown;
}) {
  return {
    id: term.id,
    name: term.name,
    lines: decodeJson<PaymentTermScheduleLineInput[]>(term.scheduleLines, []),
  } satisfies PaymentTermSnapshot;
}

export function hasCustomerRole(roles: ({ role?: string | null } | null | undefined)[]) {
  return roles.some((role) => role?.role === "CUSTOMER");
}
