import type { Querier } from '@platform-modules/db'
import {
  issueInvoice,
  type CustomerParty,
  type DocumentLineItem,
  type DocumentTaxTreatment,
  type DocumentType,
  type InvoiceProvider,
  type IssueInvoiceResult,
} from '@platform-modules/invoicing'
import { MorningProvider, type MorningCredentials } from '@platform-modules/invoicing/morning'
import {
  IL_VAT_SCHEDULE,
  fromBasisPoints,
  resolveVatRate,
  type VatRate,
} from '@platform-modules/tax/rates-table'
import { resolveTaxability, type TaxParty } from '@platform-modules/tax/taxability'

import type { pressZoneSchema } from '../schema.js'

type SupplyType = 'goods' | 'services' | 'digital'

type SettlementLineItem = Pick<DocumentLineItem, 'description' | 'quantity' | 'unitAmountMinor'>

export interface SettlementInvoiceCustomer extends CustomerParty, TaxParty {}

export interface SettlementInvoiceInput {
  supplier: TaxParty
  customer: SettlementInvoiceCustomer
  supplyType: SupplyType
  lineItems: SettlementLineItem[]
  idempotencyKey: string
}

export type SettlementInvoiceDb = Querier<typeof pressZoneSchema> & {
  readonly morningCredential: MorningCredentials
  readonly morningProvider?: InvoiceProvider
  readonly invoiceDate?: string
  readonly invoiceCurrency?: string
  readonly invoiceDocType?: DocumentType
}

const DEFAULT_CURRENCY = 'ILS'
const DEFAULT_DOC_TYPE: DocumentType = 'invoice'
const ZERO_VAT_RATE = fromBasisPoints(0)
const FIRST_IL_VAT_DATE = '1976-07-01'
const ISO_4217_CURRENCIES = new Set(['ILS', 'USD', 'EUR', 'GBP'])

export async function issueSettlementInvoice(
  db: SettlementInvoiceDb,
  input: SettlementInvoiceInput,
): Promise<IssueInvoiceResult> {
  const docType = db.invoiceDocType ?? DEFAULT_DOC_TYPE
  const invoiceDate = normalizeSettlementInvoiceDate(db.invoiceDate)
  const currency = normalizeSettlementInvoiceCurrency(db.invoiceCurrency)
  const idempotencyKey = normalizeSettlementInvoiceIdempotencyKey(input.idempotencyKey)
  const lineItems = normalizeSettlementInvoiceLineItems(input.lineItems, docType)
  const taxability = resolveTaxability({
    supplier: input.supplier,
    customer: input.customer,
    supplyType: input.supplyType,
  })
  const rate = resolveVatRateForTreatment(taxability.treatment, invoiceDate)
  const provider = db.morningProvider ?? new MorningProvider()

  return issueInvoice(db as unknown as Querier, provider, db.morningCredential, {
    customer: {
      name: input.customer.name,
      ...(input.customer.email ? { email: input.customer.email } : {}),
      ...(input.customer.taxId ? { taxId: input.customer.taxId } : {}),
    },
    lineItems: lineItems.map((item) => ({
      ...item,
      taxTreatment: toDocumentTaxTreatment(taxability.treatment),
      vatRateBasisPoints: Number(rate),
    })),
    currency,
    idempotencyKey,
    docType,
  })
}

export function normalizeSettlementInvoiceCurrency(currency?: string): string {
  const value = currency ?? DEFAULT_CURRENCY
  if (!/^[A-Z]{3}$/.test(value) || !ISO_4217_CURRENCIES.has(value)) {
    throw new Error('settlement invoice currency must be an ISO-4217 code')
  }
  return value
}

export function normalizeSettlementInvoiceDate(invoiceDate?: string): string {
  const value = invoiceDate ?? todayInIsrael()
  if (!isStrictDate(value)) {
    throw new Error('settlement invoiceDate must be a valid YYYY-MM-DD date')
  }
  if (value < FIRST_IL_VAT_DATE) {
    throw new Error(
      `settlement invoiceDate is implausibly old; earliest supported date is ${FIRST_IL_VAT_DATE}`,
    )
  }
  const today = todayInIsrael()
  if (value > today) {
    throw new Error('settlement invoiceDate must not be in the future')
  }
  return value
}

function normalizeSettlementInvoiceIdempotencyKey(idempotencyKey: string): string {
  const value = idempotencyKey.trim()
  if (value.length === 0) {
    throw new Error('settlement invoice idempotencyKey is required')
  }
  return value
}

function normalizeSettlementInvoiceLineItems(
  lineItems: SettlementLineItem[],
  docType: DocumentType,
): SettlementLineItem[] {
  if (lineItems.length === 0) {
    throw new Error('settlement invoice requires at least one line item')
  }

  return lineItems.map((item) => {
    if (!Number.isInteger(item.quantity) || item.quantity <= 0) {
      throw new Error('settlement invoice line item quantity must be greater than 0')
    }
    if (typeof item.unitAmountMinor !== 'bigint') {
      throw new Error('settlement invoice line item amount must be a non-negative integer')
    }
    if (docType !== 'credit_note' && item.unitAmountMinor < 0n) {
      throw new Error('settlement invoice line item amount must be a non-negative integer')
    }
    return item
  })
}

function resolveVatRateForTreatment(
  treatment: ReturnType<typeof resolveTaxability>['treatment'],
  date: string,
): VatRate {
  switch (treatment) {
    case 'standard':
      return resolveVatRate(IL_VAT_SCHEDULE, date)
    case 'zero_rated':
      return ZERO_VAT_RATE
    case 'exempt':
      return ZERO_VAT_RATE
    default: {
      const exhaustive: never = treatment
      return exhaustive
    }
  }
}

function toDocumentTaxTreatment(
  treatment: ReturnType<typeof resolveTaxability>['treatment'],
): DocumentTaxTreatment {
  switch (treatment) {
    case 'standard':
      return 'exclusive'
    case 'zero_rated':
      return 'zero_rated'
    case 'exempt':
      return 'exempt'
    default: {
      const exhaustive: never = treatment
      return exhaustive
    }
  }
}

function todayInIsrael(): string {
  const parts = new Intl.DateTimeFormat('en', {
    timeZone: 'Asia/Jerusalem',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).formatToParts(new Date())

  const values = Object.fromEntries(parts.map((part) => [part.type, part.value]))
  return `${values.year}-${values.month}-${values.day}`
}

function isStrictDate(value: string): boolean {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
  if (!match) return false

  const year = Number(match[1])
  const month = Number(match[2])
  const day = Number(match[3])
  const date = new Date(Date.UTC(year, month - 1, day))
  return (
    date.getUTCFullYear() === year &&
    date.getUTCMonth() === month - 1 &&
    date.getUTCDate() === day
  )
}
