import { eq } from 'drizzle-orm'
import type { Querier, Transaction, TransactionalDatabase } from '@platform-modules/db'
import { MixedCurrencyError } from '../errors.js'
import { cartReduce } from '../reduce.js'
import type { CartStore } from '../store.js'
import type { Cart, CartLine } from '../types.js'
import { CartMigrateError, isCartMigrateError, pushSchema } from './migrate.js'
import { cart, cartLine, cartSchema, type CartSchema } from './schema.js'

export { CartMigrateError, isCartMigrateError, pushSchema }
export { cart, cartLine, cartSchema, type CartSchema }

function lineFromRow(row: typeof cartLine.$inferSelect): CartLine {
  return {
    lineId: row.id,
    variantId: row.variantId,
    qty: row.qty,
    price: {
      amount: row.amount,
      currency: row.currency,
      priceMode: row.priceMode,
    },
    vendorId: row.vendorId ?? null,
  }
}

function subtotalViaCartReduce(cartId: string, lines: CartLine[]): bigint {
  let state: Cart = { id: cartId, currency: '', lines: [], subtotal: 0n }
  for (const line of lines) {
    state = cartReduce(state, {
      type: 'addLine',
      variantId: line.variantId,
      qty: line.qty,
      price: line.price,
      vendorId: line.vendorId ?? undefined,
    })
  }
  return state.subtotal
}

async function loadCart(tx: Querier<CartSchema>, cartId: string): Promise<Cart | null> {
  const [cartRow] = await tx.select().from(cart).where(eq(cart.id, cartId)).limit(1)
  if (!cartRow) return null

  const lineRows = await tx.select().from(cartLine).where(eq(cartLine.cartId, cartId))
  const lines = lineRows.map(lineFromRow)

  return {
    id: cartRow.id,
    currency: cartRow.currency,
    lines,
    subtotal: subtotalViaCartReduce(cartId, lines),
  }
}

function assertMergeCurrency(guest: Cart, user: Cart): void {
  const guestCurrency = guest.lines.length > 0 ? guest.currency : ''
  const userCurrency = user.lines.length > 0 ? user.currency : ''

  if (guestCurrency !== '' && userCurrency !== '' && guestCurrency !== userCurrency) {
    throw new MixedCurrencyError(userCurrency, guestCurrency)
  }
}

function mergeLines(userLines: CartLine[], guestLines: CartLine[]): CartLine[] {
  const merged = userLines.map((line) => ({ ...line, price: { ...line.price } }))
  const indexByVariant = new Map(merged.map((line, index) => [line.variantId, index]))

  for (const guestLine of guestLines) {
    const existingIndex = indexByVariant.get(guestLine.variantId)
    if (existingIndex === undefined) {
      indexByVariant.set(guestLine.variantId, merged.length)
      merged.push({ ...guestLine, price: { ...guestLine.price } })
      continue
    }

    const existing = merged[existingIndex]!
    merged[existingIndex] = {
      ...existing,
      qty: existing.qty + guestLine.qty,
      price: { ...existing.price },
    }
  }

  return merged
}

async function saveCart(tx: Transaction<CartSchema>, cartData: Cart): Promise<void> {
  const now = new Date()
  const [existing] = await tx.select().from(cart).where(eq(cart.id, cartData.id)).limit(1)

  if (existing) {
    await tx
      .update(cart)
      .set({ currency: cartData.currency, updatedAt: now })
      .where(eq(cart.id, cartData.id))
  } else {
    await tx.insert(cart).values({
      id: cartData.id,
      currency: cartData.currency,
      createdAt: now,
      updatedAt: now,
    })
  }

  await tx.delete(cartLine).where(eq(cartLine.cartId, cartData.id))

  if (cartData.lines.length > 0) {
    await tx.insert(cartLine).values(
      cartData.lines.map((line) => ({
        id: line.lineId,
        cartId: cartData.id,
        variantId: line.variantId,
        qty: line.qty,
        amount: line.price.amount,
        currency: line.price.currency,
        priceMode: line.price.priceMode,
        vendorId: line.vendorId,
      })),
    )
  }
}

async function findOrCreateUserCart(
  tx: Transaction<CartSchema>,
  userId: string,
): Promise<Cart> {
  const [userRow] = await tx.select().from(cart).where(eq(cart.userId, userId)).limit(1)
  if (userRow) {
    const loaded = await loadCart(tx, userRow.id)
    if (loaded) return loaded
  }

  const now = new Date()
  const [created] = await tx
    .insert(cart)
    .values({ currency: '', userId, createdAt: now, updatedAt: now })
    .returning()

  return {
    id: created!.id,
    currency: '',
    lines: [],
    subtotal: 0n,
  }
}

export function createDbCartStore(db: TransactionalDatabase<CartSchema>): CartStore {
  return {
    async load(cartId) {
      return loadCart(db, cartId)
    },

    async save(cartData) {
      await db.transaction((tx) => saveCart(tx, cartData))
    },

    async merge(guestCartId, userId) {
      return db.transaction(async (tx) => {
        const guest =
          (await loadCart(tx, guestCartId)) ?? {
            id: guestCartId,
            currency: '',
            lines: [],
            subtotal: 0n,
          }
        const user = await findOrCreateUserCart(tx, userId)

        assertMergeCurrency(guest, user)

        const currency =
          user.lines.length > 0
            ? user.currency
            : guest.lines.length > 0
              ? guest.currency
              : ''

        const lines = mergeLines(user.lines, guest.lines)
        const merged: Cart = {
          id: user.id,
          currency,
          lines,
          subtotal: subtotalViaCartReduce(user.id, lines),
        }

        await saveCart(tx, { ...merged, id: user.id })
        await tx.update(cart).set({ userId }).where(eq(cart.id, user.id))

        await tx.delete(cartLine).where(eq(cartLine.cartId, guestCartId))
        await tx.delete(cart).where(eq(cart.id, guestCartId))

        return merged
      })
    },
  }
}
