import { eq, and, count, sql, desc, type Column } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import {
  transactions,
  transactionLineItems,
  discountAudits,
  discounts,
  drawerSessions,
} from '../db/schema/pos.js'
import { products, inventoryUnits } from '../db/schema/inventory.js'
import { repairTickets, repairLineItems } from '../db/schema/repairs.js'
import { companies, locations } from '../db/schema/stores.js'
import { accounts } from '../db/schema/accounts.js'
import { NotFoundError, ValidationError, ConflictError } from '../lib/errors.js'
import { TaxService } from './tax.service.js'
import type {
  TransactionCreateInput,
  TransactionLineItemCreateInput,
  ApplyDiscountInput,
  CompleteTransactionInput,
  PaginationInput,
} from '@lunarfront/shared/schemas'
import {
  withPagination,
  withSort,
  buildSearchCondition,
  paginatedResponse,
} from '../utils/pagination.js'

export const TransactionService = {
  async create(db: PostgresJsDatabase<any>, input: TransactionCreateInput, processedBy: string) {
    const transactionNumber = await generateTransactionNumber(db)

    const [txn] = await db
      .insert(transactions)
      .values({
        transactionNumber,
        transactionType: input.transactionType,
        locationId: input.locationId,
        accountId: input.accountId,
        repairTicketId: input.repairTicketId,
        repairBatchId: input.repairBatchId,
        notes: input.notes,
        taxExempt: input.taxExempt,
        taxExemptReason: input.taxExemptReason,
        processedBy,
      })
      .returning()
    return txn
  },

  async createFromRepairTicket(db: PostgresJsDatabase<any>, ticketId: string, locationId: string | undefined, processedBy: string) {
    // Validate ticket exists and is eligible before entering transaction
    const [ticket] = await db
      .select()
      .from(repairTickets)
      .where(eq(repairTickets.id, ticketId))
      .limit(1)

    if (!ticket) throw new NotFoundError('Repair ticket')
    if (!['ready', 'approved', 'in_progress'].includes(ticket.status)) {
      throw new ValidationError('Ticket must be in ready, approved, or in_progress status to check out')
    }

    // Check for existing pending repair_payment for this ticket
    const [existing] = await db
      .select({ id: transactions.id })
      .from(transactions)
      .where(and(
        eq(transactions.repairTicketId, ticketId),
        eq(transactions.status, 'pending'),
      ))
      .limit(1)
    if (existing) throw new ConflictError('A pending transaction already exists for this ticket')

    // Fetch non-consumable line items
    const items = await db
      .select()
      .from(repairLineItems)
      .where(eq(repairLineItems.repairTicketId, ticketId))

    const billableItems = items.filter((i) => i.itemType !== 'consumable')
    if (billableItems.length === 0) throw new ValidationError('No billable line items on this ticket')

    const resolvedLocationId = locationId ?? ticket.locationId

    // Wrap creation + line items in a DB transaction for atomicity
    return db.transaction(async (tx) => {
      const transactionNumber = await generateTransactionNumber(tx)

      const [txn] = await tx
        .insert(transactions)
        .values({
          transactionNumber,
          transactionType: 'repair_payment',
          locationId: resolvedLocationId,
          accountId: ticket.accountId,
          repairTicketId: ticketId,
          processedBy,
        })
        .returning()

      for (const item of billableItems) {
        const taxCategory = TaxService.repairItemTypeToTaxCategory(item.itemType)
        let taxRate = 0
        if (resolvedLocationId) {
          taxRate = await TaxService.getRateForLocation(tx, resolvedLocationId, taxCategory)
        }

        const unitPrice = parseFloat(item.unitPrice) || 0
        const qty = Math.max(1, Math.round(parseFloat(item.qty) || 1))
        const lineSubtotal = unitPrice * qty
        const taxAmount = TaxService.calculateTax(lineSubtotal, taxRate)
        const lineTotal = lineSubtotal + taxAmount

        await tx.insert(transactionLineItems).values({
          transactionId: txn.id,
          productId: item.productId,
          description: item.description,
          qty,
          unitPrice: unitPrice.toString(),
          taxRate: taxRate.toString(),
          taxAmount: taxAmount.toString(),
          lineTotal: lineTotal.toString(),
        })
      }

      await this.recalculateTotals(tx, txn.id)

      // Return full transaction with line items
      const lineItemRows = await tx
        .select()
        .from(transactionLineItems)
        .where(eq(transactionLineItems.transactionId, txn.id))

      // Re-read the transaction to get updated totals
      const [updated] = await tx
        .select()
        .from(transactions)
        .where(eq(transactions.id, txn.id))
        .limit(1)

      return { ...updated, lineItems: lineItemRows }
    })
  },

  async addLineItem(db: PostgresJsDatabase<any>, transactionId: string, input: TransactionLineItemCreateInput) {
    const txn = await this.getById(db, transactionId)
    if (!txn) throw new NotFoundError('Transaction')
    if (txn.status !== 'pending') throw new ConflictError('Can only add items to pending transactions')

    // Resolve tax category from the product (defaults to "goods")
    let taxCategory: 'goods' | 'service' | 'exempt' = 'goods'
    if (input.productId) {
      const [product] = await db
        .select({ taxCategory: products.taxCategory })
        .from(products)
        .where(eq(products.id, input.productId))
        .limit(1)
      if (product?.taxCategory) {
        taxCategory = product.taxCategory as 'goods' | 'service' | 'exempt'
      }
    }

    // Snapshot the tax rate at time of sale
    let taxRate = 0
    if (!txn.taxExempt && txn.locationId) {
      taxRate = await TaxService.getRateForLocation(db, txn.locationId, taxCategory)
    }

    const lineSubtotal = input.unitPrice * input.qty
    const taxAmount = TaxService.calculateTax(lineSubtotal, taxRate)
    const lineTotal = lineSubtotal + taxAmount

    const [lineItem] = await db
      .insert(transactionLineItems)
      .values({
        transactionId,
        productId: input.productId,
        inventoryUnitId: input.inventoryUnitId,
        description: input.description,
        qty: input.qty,
        unitPrice: input.unitPrice.toString(),
        taxRate: taxRate.toString(),
        taxAmount: taxAmount.toString(),
        lineTotal: lineTotal.toString(),
      })
      .returning()

    await this.recalculateTotals(db, transactionId)
    return lineItem
  },

  async removeLineItem(db: PostgresJsDatabase<any>, transactionId: string, lineItemId: string) {
    const txn = await this.getById(db, transactionId)
    if (!txn) throw new NotFoundError('Transaction')
    if (txn.status !== 'pending') throw new ConflictError('Can only remove items from pending transactions')

    const [deleted] = await db
      .delete(transactionLineItems)
      .where(
        and(
          eq(transactionLineItems.id, lineItemId),
          eq(transactionLineItems.transactionId, transactionId)
        )
      )
      .returning()

    if (!deleted) throw new NotFoundError('Line item')

    await this.recalculateTotals(db, transactionId)
    return deleted
  },

  async applyDiscount(
    db: PostgresJsDatabase<any>,
    transactionId: string,
    input: ApplyDiscountInput,
    appliedBy: string,
  ) {
    const txn = await this.getById(db, transactionId)
    if (!txn) throw new NotFoundError('Transaction')
    if (txn.status !== 'pending') throw new ConflictError('Can only apply discounts to pending transactions')

    // If applying a predefined discount, check approval threshold
    if (input.discountId) {
      const [discount] = await db
        .select()
        .from(discounts)
        .where(eq(discounts.id, input.discountId))
        .limit(1)

      if (!discount) throw new NotFoundError('Discount')

      if (
        discount.requiresApprovalAbove &&
        input.amount > parseFloat(discount.requiresApprovalAbove)
      ) {
        throw new ValidationError('Discount amount exceeds approval threshold — manager approval required')
      }
    }

    // Apply to specific line item or order level
    if (input.lineItemId) {
      const [lineItem] = await db
        .select()
        .from(transactionLineItems)
        .where(
          and(
            eq(transactionLineItems.id, input.lineItemId),
            eq(transactionLineItems.transactionId, transactionId)
          )
        )
        .limit(1)

      if (!lineItem) throw new NotFoundError('Line item')

      const originalAmount = parseFloat(lineItem.unitPrice) * lineItem.qty

      await db
        .update(transactionLineItems)
        .set({
          discountAmount: input.amount.toString(),
          discountReason: input.reason,
        })
        .where(eq(transactionLineItems.id, input.lineItemId))

      // Recalculate line total (subtotal - discount + tax)
      const lineSubtotal = originalAmount - input.amount
      const taxAmount = TaxService.calculateTax(lineSubtotal, parseFloat(lineItem.taxRate))
      const lineTotal = lineSubtotal + taxAmount

      await db
        .update(transactionLineItems)
        .set({
          taxAmount: taxAmount.toString(),
          lineTotal: lineTotal.toString(),
        })
        .where(eq(transactionLineItems.id, input.lineItemId))

      // Create audit record
      await db.insert(discountAudits).values({
        transactionId,
        transactionLineItemId: input.lineItemId,
        discountId: input.discountId,
        appliedBy,
        originalAmount: originalAmount.toString(),
        discountedAmount: input.amount.toString(),
        reason: input.reason,
      })
    }

    await this.recalculateTotals(db, transactionId)
  },

  async recalculateTotals(db: PostgresJsDatabase<any>, transactionId: string) {
    const lineItems = await db
      .select()
      .from(transactionLineItems)
      .where(eq(transactionLineItems.transactionId, transactionId))

    let subtotal = 0
    let discountTotal = 0
    let taxTotal = 0

    for (const item of lineItems) {
      const itemSubtotal = parseFloat(item.unitPrice) * item.qty
      subtotal += itemSubtotal
      discountTotal += parseFloat(item.discountAmount)
      taxTotal += parseFloat(item.taxAmount)
    }

    const total = subtotal - discountTotal + taxTotal

    await db
      .update(transactions)
      .set({
        subtotal: subtotal.toString(),
        discountTotal: discountTotal.toString(),
        taxTotal: taxTotal.toString(),
        total: total.toString(),
        updatedAt: new Date(),
      })
      .where(eq(transactions.id, transactionId))
  },

  async complete(db: PostgresJsDatabase<any>, transactionId: string, input: CompleteTransactionInput) {
    const txn = await this.getById(db, transactionId)
    if (!txn) throw new NotFoundError('Transaction')
    if (txn.status !== 'pending') throw new ConflictError('Transaction is not pending')

    // Require an open drawer session at the transaction's location
    let drawerSessionId: string | null = null
    if (txn.locationId) {
      const [openDrawer] = await db
        .select({ id: drawerSessions.id })
        .from(drawerSessions)
        .where(and(eq(drawerSessions.locationId, txn.locationId), eq(drawerSessions.status, 'open')))
        .limit(1)
      if (!openDrawer) {
        throw new ValidationError('Cannot complete transaction without an open drawer at this location')
      }
      drawerSessionId = openDrawer.id
    }

    // Validate cash payment (with optional nickel rounding)
    let changeGiven: string | undefined
    let roundingAdjustment = 0
    if (input.paymentMethod === 'cash') {
      let total = parseFloat(txn.total)

      // Apply Swedish rounding if location has cash_rounding enabled
      if (txn.locationId) {
        const [loc] = await db
          .select({ cashRounding: locations.cashRounding })
          .from(locations)
          .where(eq(locations.id, txn.locationId))
          .limit(1)
        if (loc?.cashRounding) {
          const rounded = TaxService.roundToNickel(total)
          roundingAdjustment = Math.round((rounded - total) * 100) / 100
          total = rounded
        }
      }

      if (!input.amountTendered || input.amountTendered < total) {
        throw new ValidationError('Amount tendered must be >= transaction total for cash payments')
      }
      changeGiven = (input.amountTendered - total).toString()
    }

    // Wrap inventory updates, transaction completion, and repair status in a DB transaction
    return db.transaction(async (tx) => {
      const lineItems = await tx
        .select()
        .from(transactionLineItems)
        .where(eq(transactionLineItems.transactionId, transactionId))

      for (const item of lineItems) {
        if (item.inventoryUnitId) {
          await tx
            .update(inventoryUnits)
            .set({ status: 'sold' })
            .where(eq(inventoryUnits.id, item.inventoryUnitId))
        } else if (item.productId) {
          await tx
            .update(products)
            .set({
              qtyOnHand: sql`${products.qtyOnHand} - ${item.qty}`,
              updatedAt: new Date(),
            })
            .where(eq(products.id, item.productId))
        }
      }

      const [completed] = await tx
        .update(transactions)
        .set({
          status: 'completed',
          paymentMethod: input.paymentMethod,
          amountTendered: input.amountTendered?.toString(),
          changeGiven,
          roundingAdjustment: roundingAdjustment.toString(),
          checkNumber: input.checkNumber,
          drawerSessionId,
          completedAt: new Date(),
          updatedAt: new Date(),
        })
        .where(eq(transactions.id, transactionId))
        .returning()

      // If this is a repair payment, update ticket status to picked_up
      if (completed.transactionType === 'repair_payment' && completed.repairTicketId) {
        await tx
          .update(repairTickets)
          .set({ status: 'picked_up', completedDate: new Date(), updatedAt: new Date() })
          .where(eq(repairTickets.id, completed.repairTicketId))
      }

      return completed
    })
  },

  async void(db: PostgresJsDatabase<any>, transactionId: string, _voidedBy: string) {
    const txn = await this.getById(db, transactionId)
    if (!txn) throw new NotFoundError('Transaction')
    if (txn.status !== 'pending') throw new ConflictError('Can only void pending transactions')

    // Restore inventory (in case items were reserved, though we only decrement on complete)
    const [voided] = await db
      .update(transactions)
      .set({
        status: 'voided',
        updatedAt: new Date(),
      })
      .where(eq(transactions.id, transactionId))
      .returning()

    return voided
  },

  async getById(db: PostgresJsDatabase<any>, id: string) {
    const [txn] = await db
      .select()
      .from(transactions)
      .where(eq(transactions.id, id))
      .limit(1)

    if (!txn) return null

    const lineItems = await db
      .select()
      .from(transactionLineItems)
      .where(eq(transactionLineItems.transactionId, id))

    return { ...txn, lineItems }
  },

  async getReceipt(db: PostgresJsDatabase<any>, id: string) {
    const txn = await this.getById(db, id)
    if (!txn) throw new NotFoundError('Transaction')

    // Get company info
    const [company] = await db.select().from(companies).limit(1)

    // Get location info if available
    let location = null
    if (txn.locationId) {
      const [loc] = await db
        .select()
        .from(locations)
        .where(eq(locations.id, txn.locationId))
        .limit(1)
      location = loc ?? null
    }

    // Resolve customer email from linked account
    let customerEmail: string | null = null
    if (txn.accountId) {
      const [acct] = await db.select({ email: accounts.email }).from(accounts).where(eq(accounts.id, txn.accountId)).limit(1)
      customerEmail = acct?.email ?? null
    }

    return {
      transaction: txn,
      customerEmail,
      company: company
        ? {
            name: company.name,
            phone: company.phone,
            email: company.email,
            address: company.address,
          }
        : null,
      location: location
        ? {
            name: location.name,
            phone: location.phone,
            email: location.email,
            address: location.address,
          }
        : null,
    }
  },

  async list(db: PostgresJsDatabase<any>, params: PaginationInput, filters?: {
    status?: string
    transactionType?: string
    locationId?: string
    accountId?: string
    itemSearch?: string
  }) {
    const conditions: ReturnType<typeof eq>[] = []

    if (params.q) {
      conditions.push(buildSearchCondition(params.q, [transactions.transactionNumber])!)
    }
    if (filters?.itemSearch) {
      const term = `%${filters.itemSearch}%`
      conditions.push(
        sql`EXISTS (SELECT 1 FROM ${transactionLineItems} WHERE ${transactionLineItems.transactionId} = ${transactions.id} AND ${transactionLineItems.description} ILIKE ${term})`
      )
    }
    if (filters?.status) {
      conditions.push(eq(transactions.status, filters.status as any))
    }
    if (filters?.transactionType) {
      conditions.push(eq(transactions.transactionType, filters.transactionType as any))
    }
    if (filters?.locationId) {
      conditions.push(eq(transactions.locationId, filters.locationId))
    }
    if (filters?.accountId) {
      conditions.push(eq(transactions.accountId, filters.accountId))
    }

    const where = conditions.length === 0 ? undefined : conditions.length === 1 ? conditions[0] : and(...conditions)

    const sortableColumns: Record<string, Column> = {
      transaction_number: transactions.transactionNumber,
      total: transactions.total,
      status: transactions.status,
      created_at: transactions.createdAt,
      completed_at: transactions.completedAt,
    }

    let query = db.select().from(transactions).where(where).$dynamic()
    query = withSort(query, params.sort, params.order, sortableColumns, transactions.createdAt)
    query = withPagination(query, params.page, params.limit)

    const countQuery = where
      ? db.select({ total: count() }).from(transactions).where(where)
      : db.select({ total: count() }).from(transactions)

    const [data, [{ total }]] = await Promise.all([query, countQuery])

    return paginatedResponse(data, total, params.page, params.limit)
  },
}

async function generateTransactionNumber(db: PostgresJsDatabase<any>): Promise<string> {
  const today = new Date()
  const dateStr = today.toISOString().slice(0, 10).replace(/-/g, '')
  const prefix = `TXN-${dateStr}-`

  // Find the highest sequence number for today
  const [latest] = await db
    .select({ transactionNumber: transactions.transactionNumber })
    .from(transactions)
    .where(sql`${transactions.transactionNumber} LIKE ${prefix + '%'}`)
    .orderBy(desc(transactions.transactionNumber))
    .limit(1)

  let seq = 1
  if (latest?.transactionNumber) {
    const lastSeq = parseInt(latest.transactionNumber.replace(prefix, ''), 10)
    if (!isNaN(lastSeq)) seq = lastSeq + 1
  }

  return `${prefix}${seq.toString().padStart(4, '0')}`
}
