import type { Querier } from '@platform-modules/db'
import { sql } from 'drizzle-orm'
import { toInvoiceError, type InvoiceError } from './errors.js'
import type { InvoiceProvider } from './port.js'
import type { DocumentSpec, InvoiceDocumentResult } from './types.js'

export type IssueInvoiceResult =
  | { ok: true; result: InvoiceDocumentResult }
  | { ok: false; error: InvoiceError }

const PENDING_SENTINEL = ''
const CLAIM_POLL_ATTEMPTS = 20
const CLAIM_POLL_DELAY_MS = 250
const CLAIM_STALE_AFTER = sql.raw(`INTERVAL '5 minutes'`)

export async function issueInvoice(
  db: Querier,
  provider: InvoiceProvider,
  credential: unknown,
  spec: DocumentSpec,
): Promise<IssueInvoiceResult> {
  const claimToken = crypto.randomUUID()
  const existing = await loadStoredResult(db, spec.idempotencyKey)
  if (existing !== null) {
    return { ok: true, result: existing }
  }

  const claimed = await claimIdempotencyKey(db, spec.idempotencyKey, claimToken)
  if (!claimed) {
    const winner = await awaitClaimResult(db, spec.idempotencyKey)
    if (winner !== null) {
      return { ok: true, result: winner }
    }
    if (await requiresReconciliation(db, spec.idempotencyKey)) {
      return {
        ok: false,
        error: {
          code: 'RECONCILE_REQUIRED',
          issued: 'unknown',
          message: 'invoice issuance requires provider reconciliation; the idempotency key is stamped in the provider document remarks as the reconciliation handle',
        },
      }
    }
    return {
      ok: false,
      error: { code: 'PROVIDER_REJECTED', message: 'concurrent invoice issuance in progress' },
    }
  }

  let result: InvoiceDocumentResult
  try {
    const stamp = await db.execute(sql`
      UPDATE invoice_document
      SET provider_called_at = NOW()
      WHERE idempotency_key = ${spec.idempotencyKey}
        AND document_id = ${PENDING_SENTINEL}
        AND claim_token = ${claimToken}
      RETURNING idempotency_key
    `)
    if (unwrapRows<{ idempotency_key: string }>(stamp).length === 0) {
      return {
        ok: false,
        error: { code: 'PROVIDER_REJECTED', message: 'concurrent invoice issuance in progress' },
      }
    }
    result = await provider.issue(credential, spec)
  } catch (error) {
    const invoiceError = toInvoiceError(error)
    if (invoiceError.issued === 'no') {
      const released = await releaseClaim(db, spec.idempotencyKey, claimToken)
      if (!released) {
        return {
          ok: false,
          error: { code: 'PROVIDER_REJECTED', message: 'concurrent invoice issuance in progress' },
        }
      }
    }
    return { ok: false, error: invoiceError }
  }

  try {
    const recorded = await db.execute(sql`
      UPDATE invoice_document
      SET document_id = ${result.documentId},
          document_number = ${result.documentNumber},
          document_url = ${result.documentUrl}
      WHERE idempotency_key = ${spec.idempotencyKey}
        AND claim_token = ${claimToken}
      RETURNING idempotency_key
    `)
    if (unwrapRows<{ idempotency_key: string }>(recorded).length === 0) {
      return {
        ok: false,
        error: {
          code: 'PROVIDER_REJECTED',
          issued: 'unknown',
          message: 'issued invoice result could not be recorded',
        },
      }
    }
    return { ok: true, result }
  } catch (error) {
    // A real document was already issued at the provider — never release
    // the claim here, or a retry would call the provider again.
    return { ok: false, error: { ...toInvoiceError(error), issued: 'unknown' } }
  }
}

/**
 * Atomically claims the idempotency key via the unique constraint BEFORE
 * calling the provider — prevents two concurrent callers both issuing a
 * real document at the provider (the DB row alone can't dedupe a call that
 * already happened by the time it's inserted). A pending claim older than
 * the staleness window is reclaimable, so a crash (or a failed recording
 * write) between the provider call and the DB write can't permanently wedge
 * the key.
 */
async function claimIdempotencyKey(
  db: Querier,
  idempotencyKey: string,
  claimToken: string,
): Promise<boolean> {
  const result = await db.execute(sql`
    INSERT INTO invoice_document (
      idempotency_key,
      document_id,
      document_number,
      document_url,
      claim_token
    )
    VALUES (
      ${idempotencyKey},
      ${PENDING_SENTINEL},
      ${PENDING_SENTINEL},
      ${PENDING_SENTINEL},
      ${claimToken}
    )
    ON CONFLICT (idempotency_key) DO UPDATE
      SET created_at = NOW(),
          provider_called_at = NULL,
          claim_token = ${claimToken}
      WHERE invoice_document.document_id = ${PENDING_SENTINEL}
        AND invoice_document.created_at < NOW() - ${CLAIM_STALE_AFTER}
        AND invoice_document.provider_called_at IS NULL
    RETURNING idempotency_key
  `)
  return unwrapRows<{ idempotency_key: string }>(result).length > 0
}

async function releaseClaim(
  db: Querier,
  idempotencyKey: string,
  claimToken: string,
): Promise<boolean> {
  const result = await db.execute(sql`
    DELETE FROM invoice_document
    WHERE idempotency_key = ${idempotencyKey}
      AND document_id = ${PENDING_SENTINEL}
      AND claim_token = ${claimToken}
    RETURNING idempotency_key
  `)
  return unwrapRows<{ idempotency_key: string }>(result).length > 0
}

async function awaitClaimResult(
  db: Querier,
  idempotencyKey: string,
): Promise<InvoiceDocumentResult | null> {
  for (let attempt = 0; attempt < CLAIM_POLL_ATTEMPTS; attempt++) {
    const stored = await loadStoredResult(db, idempotencyKey)
    if (stored !== null) {
      return stored
    }
    await new Promise((resolve) => setTimeout(resolve, CLAIM_POLL_DELAY_MS))
  }
  return null
}

async function requiresReconciliation(db: Querier, idempotencyKey: string): Promise<boolean> {
  const result = await db.execute(sql`
    SELECT idempotency_key
    FROM invoice_document
    WHERE idempotency_key = ${idempotencyKey}
      AND document_id = ${PENDING_SENTINEL}
      AND created_at < NOW() - ${CLAIM_STALE_AFTER}
      AND provider_called_at IS NOT NULL
    LIMIT 1
  `)
  return unwrapRows<{ idempotency_key: string }>(result).length > 0
}

async function loadStoredResult(
  db: Querier,
  idempotencyKey: string,
): Promise<InvoiceDocumentResult | null> {
  const result = await db.execute(sql`
    SELECT
      document_id AS "documentId",
      document_number AS "documentNumber",
      document_url AS "documentUrl"
    FROM invoice_document
    WHERE idempotency_key = ${idempotencyKey} AND document_id != ${PENDING_SENTINEL}
    LIMIT 1
  `)

  const rows = unwrapRows<InvoiceDocumentResult>(result)
  return rows[0] ?? null
}

function unwrapRows<T>(result: unknown): T[] {
  return (Array.isArray(result) ? result : (result as { rows?: T[] }).rows) ?? []
}

export { isInvoiceError, toInvoiceError } from './errors.js'
export { MockInvoiceProvider } from './mock-provider.js'
export type { InvoiceError, InvoiceErrorCode } from './errors.js'
export type { InvoiceProvider } from './port.js'
export type {
  CustomerParty,
  DocumentLineItem,
  DocumentSpec,
  DocumentTaxTreatment,
  DocumentType,
  InvoiceDocumentResult,
} from './types.js'
