import type { PoolClient, QueryResult } from 'pg'

import { query, withTransaction } from '../db/client.js'
import type { OrderItemReplaceInput } from '../types/repository/inputs.js'
import type { OrderItemRow } from '../types/repository/rows.js'

export type QueryExecutor = (text: string, params?: unknown[]) => Promise<QueryResult<any>>

type OrderItemSyncPlan = {
  updates: Array<{
    orderItemId: number
    item: OrderItemReplaceInput
  }>
  creates: OrderItemReplaceInput[]
  deletes: number[]
}

type OrderItemIdentityInput = Partial<OrderItemReplaceInput> & Partial<OrderItemRow>

export async function listOrderItemsByOrderId(orderId: number | string): Promise<OrderItemRow[]> {
  return listOrderItemsByOrderIdWithExecutor(query, orderId)
}

export async function listOrderItemsByOrderIds(orderIds: number[]): Promise<OrderItemRow[]> {
  const normalizedIds = orderIds.map(Number).filter((value) => value > 0)
  if (normalizedIds.length === 0) return []

  const result = await query<OrderItemRow>(
    `
      SELECT *
      FROM order_items
      WHERE order_id = ANY($1::bigint[])
      ORDER BY order_id ASC, id ASC
    `,
    [normalizedIds],
  )
  return result.rows
}

export async function replaceOrderItems(
  orderId: number | string,
  items: OrderItemReplaceInput[],
): Promise<OrderItemRow[]> {
  return withTransaction((client: PoolClient) =>
    replaceOrderItemsWithExecutor(client.query.bind(client), orderId, items),
  )
}

export async function replaceOrderItemsWithExecutor(
  executor: QueryExecutor,
  orderId: number | string,
  items: OrderItemReplaceInput[],
): Promise<OrderItemRow[]> {
  const existingItems = await listOrderItemsByOrderIdWithExecutor(executor, orderId)
  const plan = resolveOrderItemSyncPlan(existingItems, items)

  for (const update of plan.updates) {
    await executor(
      `
          UPDATE order_items
          SET
            sku_code = $1,
            sku_name = $2,
            quantity = $3,
            spec_json = $4::jsonb,
            item_snapshot_json = $5::jsonb,
            updated_at = $6
          WHERE id = $7
        `,
      [
        update.item.skuCode,
        update.item.skuName,
        update.item.quantity,
        update.item.specJson || '{}',
        update.item.itemSnapshotJson || update.item.specJson || '{}',
        update.item.updatedAt,
        update.orderItemId,
      ],
    )
  }

  for (const create of plan.creates) {
    await executor(
      `
          INSERT INTO order_items (
            order_id,
            sku_code,
            sku_name,
            quantity,
            spec_json,
            item_snapshot_json,
            created_at,
            updated_at
          ) VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7, $8)
        `,
      [
        Number(orderId),
        create.skuCode,
        create.skuName,
        create.quantity,
        create.specJson || '{}',
        create.itemSnapshotJson || create.specJson || '{}',
        create.createdAt,
        create.updatedAt,
      ],
    )
  }

  if (plan.deletes.length > 0) {
    const deletableIds = await listDeletableOrderItemIdsWithExecutor(executor, plan.deletes)

    if (deletableIds.length > 0) {
      await executor('DELETE FROM order_items WHERE id = ANY($1::bigint[])', [deletableIds])
    }
  }

  return listOrderItemsByOrderIdWithExecutor(executor, orderId)
}

export function resolveOrderItemSyncPlan(
  existingItems: OrderItemRow[],
  nextItems: OrderItemReplaceInput[],
): OrderItemSyncPlan {
  const normalizedExisting = Array.isArray(existingItems) ? existingItems : []
  const normalizedNext = Array.isArray(nextItems) ? nextItems : []
  const unmatchedExisting = [...normalizedExisting]
  const updates: OrderItemSyncPlan['updates'] = []
  const creates: OrderItemReplaceInput[] = []

  for (const item of normalizedNext) {
    const matchedIndex = findMatchingExistingOrderItemIndex(unmatchedExisting, item)

    if (matchedIndex >= 0) {
      const matched = unmatchedExisting.splice(matchedIndex, 1)[0]
      if (!matched) {
        creates.push(item)
        continue
      }
      updates.push({
        orderItemId: matched.id,
        item,
      })
      continue
    }

    if (unmatchedExisting.length > 0) {
      const matched = unmatchedExisting.shift()
      if (matched) {
        updates.push({
          orderItemId: matched.id,
          item,
        })
      }
      continue
    }

    creates.push(item)
  }

  return {
    updates,
    creates,
    deletes: unmatchedExisting.map((item) => Number(item.id)).filter((value) => value > 0),
  }
}

function findMatchingExistingOrderItemIndex(
  existingItems: OrderItemRow[],
  nextItem: OrderItemReplaceInput,
): number {
  const nextIdentity = buildOrderItemIdentity(nextItem)

  return existingItems.findIndex((item) => buildOrderItemIdentity(item) === nextIdentity)
}

function buildOrderItemIdentity(item: OrderItemIdentityInput): string {
  return [
    String(item.skuCode ?? item.sku_code ?? '').trim(),
    String(item.skuName ?? item.sku_name ?? '').trim(),
    String(Math.max(1, Number(item.quantity || 1))),
  ].join('::')
}

async function listDeletableOrderItemIdsWithExecutor(
  executor: QueryExecutor,
  orderItemIds: number[],
): Promise<number[]> {
  const normalizedIds = Array.isArray(orderItemIds)
    ? orderItemIds.map((value) => Number(value)).filter((value) => value > 0)
    : []

  if (normalizedIds.length === 0) {
    return []
  }

  const result = await executor(
    `
      SELECT oi.id
      FROM order_items oi
      LEFT JOIN fulfillment_tasks ft ON ft.order_item_id = oi.id
      WHERE oi.id = ANY($1::bigint[])
      GROUP BY oi.id
      HAVING COUNT(ft.id) = 0
      ORDER BY oi.id ASC
    `,
    [normalizedIds],
  )

  return result.rows.map((row) => Number(row.id)).filter((value) => value > 0)
}

async function listOrderItemsByOrderIdWithExecutor(
  executor: QueryExecutor,
  orderId: number | string,
): Promise<OrderItemRow[]> {
  const result = await executor(
    `
      SELECT *
      FROM order_items
      WHERE order_id = $1
      ORDER BY id ASC
    `,
    [Number(orderId)],
  )

  return result.rows
}

export async function getOrderItemById(orderItemId: number | string): Promise<OrderItemRow | null> {
  const result = await query<OrderItemRow>('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [
    Number(orderItemId),
  ])
  return result.rows[0] || null
}

export async function updateOrderItemSourceSnapshot(
  orderItemId: number | string,
  input: { skuName: string; itemSnapshotJson: string; updatedAt: string },
): Promise<OrderItemRow | null> {
  const result = await query<{ id: number }>(
    `
      UPDATE order_items
      SET sku_name = $1, item_snapshot_json = $2::jsonb, updated_at = $3
      WHERE id = $4
      RETURNING id
    `,
    [input.skuName, input.itemSnapshotJson, input.updatedAt, Number(orderItemId)],
  )
  return result.rows[0] ? getOrderItemById(orderItemId) : null
}
