import { CartValidationError, MixedCurrencyError } from './errors.js'
import type { Cart, CartAction, CartLine } from './types.js'

function computeSubtotal(lines: CartLine[]): bigint {
  return lines.reduce((sum, line) => sum + line.price.amount * BigInt(line.qty), 0n)
}

function cloneCart(cart: Cart): Cart {
  return {
    id: cart.id,
    currency: cart.currency,
    lines: cart.lines.map((line) => ({ ...line, price: { ...line.price } })),
    subtotal: cart.subtotal,
  }
}

function assertPositiveIntegerQty(qty: number): void {
  if (!Number.isInteger(qty) || qty <= 0) {
    throw new CartValidationError('qty')
  }
}

function assertCurrencyMatch(cartCurrency: string, lineCurrency: string): void {
  if (cartCurrency !== '' && lineCurrency !== cartCurrency) {
    throw new MixedCurrencyError(cartCurrency, lineCurrency)
  }
}

export function cartReduce(state: Cart, action: CartAction): Cart {
  switch (action.type) {
    case 'addLine': {
      assertPositiveIntegerQty(action.qty)
      assertCurrencyMatch(state.currency, action.price.currency)

      const currency =
        state.lines.length === 0 ? action.price.currency : state.currency
      const vendorId = action.vendorId ?? null
      const existingIndex = state.lines.findIndex(
        (line) => line.variantId === action.variantId,
      )

      let lines: CartLine[]
      if (existingIndex >= 0) {
        lines = state.lines.map((line, index) =>
          index === existingIndex
            ? {
                ...line,
                qty: line.qty + action.qty,
                price: { ...action.price },
                vendorId,
              }
            : { ...line, price: { ...line.price } },
        )
      } else {
        lines = [
          ...state.lines.map((line) => ({ ...line, price: { ...line.price } })),
          {
            lineId: crypto.randomUUID(),
            variantId: action.variantId,
            qty: action.qty,
            price: { ...action.price },
            vendorId,
          },
        ]
      }

      return {
        id: state.id,
        currency,
        lines,
        subtotal: computeSubtotal(lines),
      }
    }

    case 'setQty': {
      assertPositiveIntegerQty(action.qty)

      const lineIndex = state.lines.findIndex((line) => line.lineId === action.lineId)
      if (lineIndex < 0) {
        throw new CartValidationError('lineId')
      }

      const lines = state.lines.map((line, index) =>
        index === lineIndex ? { ...line, qty: action.qty, price: { ...line.price } } : { ...line, price: { ...line.price } },
      )

      return {
        id: state.id,
        currency: state.currency,
        lines,
        subtotal: computeSubtotal(lines),
      }
    }

    case 'removeLine': {
      const lineIndex = state.lines.findIndex((line) => line.lineId === action.lineId)
      if (lineIndex < 0) {
        return cloneCart(state)
      }

      const lines = state.lines
        .filter((line) => line.lineId !== action.lineId)
        .map((line) => ({ ...line, price: { ...line.price } }))

      return {
        id: state.id,
        currency: state.lines.length === 1 ? '' : state.currency,
        lines,
        subtotal: computeSubtotal(lines),
      }
    }

    case 'clear': {
      return {
        id: state.id,
        currency: '',
        lines: [],
        subtotal: 0n,
      }
    }
  }
}

export function groupByVendor(
  cart: Cart,
): Array<{ vendorId: string | null; lines: CartLine[]; subtotal: bigint }> {
  const groups: Array<{ vendorId: string | null; lines: CartLine[]; subtotal: bigint }> = []
  const indexByVendor = new Map<string | null, number>()

  for (const line of cart.lines) {
    const vendorId = line.vendorId
    const existingIndex = indexByVendor.get(vendorId)
    if (existingIndex === undefined) {
      indexByVendor.set(vendorId, groups.length)
      groups.push({
        vendorId,
        lines: [{ ...line, price: { ...line.price } }],
        subtotal: line.price.amount * BigInt(line.qty),
      })
    } else {
      const group = groups[existingIndex]!
      group.lines.push({ ...line, price: { ...line.price } })
      group.subtotal += line.price.amount * BigInt(line.qty)
    }
  }

  return groups
}
