/**
 * Contract query helpers — contracts-esignature (wave-8 leaf 4).
 *
 * All helpers are tenant-filtered. Route files import from '@zync/db/queries'.
 * Audit rows are inserted in the same transaction as every write.
 */
import { and, eq, desc } from 'drizzle-orm'
import type { Db } from '../client'
import { assertTenantOwnsOrThrow, assertTenantOwnsCustomer } from './tenant-guards'
import { contracts } from '../schema/contracts'
import type { ContractRow } from '../schema/contracts'
import { auditLog } from './_audit-forward'
import { captureEntityChange } from './entity-history'

/** Optional actor context for operational-audit-trail diff capture. */
export interface ContractActorContext {
  actorName?: string | null
  actorEmail?: string | null
  ipAddress?: string | null
}

// ── Public interfaces ─────────────────────────────────────────────────────────

export interface Contract {
  id: string
  tenantId: string
  customerId: string | null
  title: string
  content: string
  status: 'draft' | 'sent' | 'signed' | 'voided'
  signerEmail: string | null
  signerName: string | null
  sentAt: string | null
  signedAt: string | null
  voidedAt: string | null
  signatureData: unknown
  metadata: unknown
  createdAt: string
  updatedAt: string
}

export interface ContractListResponse {
  items: Contract[]
}

export class ContractNotFoundError extends Error {
  constructor(id: string) {
    super(`Contract not found: ${id}`)
    this.name = 'ContractNotFoundError'
  }
}

// ── Serializer ────────────────────────────────────────────────────────────────

function serializeRow(row: ContractRow): Contract {
  return {
    id: row.id,
    tenantId: row.tenantId,
    customerId: row.customerId ?? null,
    title: row.title,
    content: row.content,
    status: row.status as Contract['status'],
    signerEmail: row.signerEmail ?? null,
    signerName: row.signerName ?? null,
    sentAt: row.sentAt ? row.sentAt.toISOString() : null,
    signedAt: row.signedAt ? row.signedAt.toISOString() : null,
    voidedAt: row.voidedAt ? row.voidedAt.toISOString() : null,
    signatureData: row.signatureData ?? null,
    metadata: row.metadata ?? {},
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
  }
}

// ── getContractPublic (by id only — for public sign endpoint) ─────────────────

export async function getContractPublic(
  db: Db,
  contractId: string,
): Promise<Contract | null> {
  const rows = await db
    .select()
    .from(contracts)
    .where(eq(contracts.id, contractId))
    .limit(1)

  if (rows.length === 0) return null
  return serializeRow(rows[0]!)
}

// ── listContracts ─────────────────────────────────────────────────────────────

export async function listContracts(
  db: Db,
  tenantId: string,
  filter?: { status?: string },
): Promise<ContractListResponse> {
  const conditions = [eq(contracts.tenantId, tenantId)]
  if (filter?.status) {
    conditions.push(eq(contracts.status, filter.status))
  }

  const rows = await db
    .select()
    .from(contracts)
    .where(and(...conditions))
    .orderBy(desc(contracts.createdAt))
    .limit(100)

  return { items: rows.map(serializeRow) }
}

// ── getContract ───────────────────────────────────────────────────────────────

export async function getContract(
  db: Db,
  tenantId: string,
  contractId: string,
): Promise<Contract> {
  const rows = await db
    .select()
    .from(contracts)
    .where(and(eq(contracts.tenantId, tenantId), eq(contracts.id, contractId)))
    .limit(1)

  if (rows.length === 0) throw new ContractNotFoundError(contractId)
  return serializeRow(rows[0]!)
}

// ── createContract ────────────────────────────────────────────────────────────

export async function createContract(
  db: Db,
  tenantId: string,
  userId: string,
  data: { title: string; content: string; customerId?: string | null },
): Promise<Contract> {
  return db.transaction(async (tx) => {
    assertTenantOwnsOrThrow(
      'customerId',
      await assertTenantOwnsCustomer(tx, tenantId, data.customerId),
    )

    const now = new Date()
    const [row] = await tx
      .insert(contracts)
      .values({
        tenantId,
        customerId: data.customerId ?? null,
        title: data.title,
        content: data.content,
        status: 'draft',
        createdAt: now,
        updatedAt: now,
      })
      .returning()

    if (!row) throw new Error('Insert failed')

    await tx.insert(auditLog).values({
      tenantId,
      actorId: userId,
      actorType: 'user',
      entityType: 'contract',
      entityId: row.id,
      action: 'contract.created',
      changes: null,
    })

    return serializeRow(row)
  })
}

// ── sendContract ──────────────────────────────────────────────────────────────

export async function sendContract(
  db: Db,
  tenantId: string,
  contractId: string,
  signerEmail: string,
  signerName: string,
  userId: string,
  actorCtx?: ContractActorContext,
): Promise<Contract> {
  return db.transaction(async (tx) => {
    const existing = await tx
      .select()
      .from(contracts)
      .where(and(eq(contracts.tenantId, tenantId), eq(contracts.id, contractId)))
      .limit(1)

    if (existing.length === 0) throw new ContractNotFoundError(contractId)
    if (existing[0]!.status !== 'draft') {
      throw new Error(`Cannot send contract in status: ${existing[0]!.status}`)
    }

    const now = new Date()
    const [row] = await tx
      .update(contracts)
      .set({ status: 'sent', signerEmail, signerName, sentAt: now, updatedAt: now })
      .where(and(eq(contracts.tenantId, tenantId), eq(contracts.id, contractId)))
      .returning()

    if (!row) throw new Error('Update failed')

    await tx.insert(auditLog).values({
      tenantId,
      actorId: userId,
      actorType: 'user',
      entityType: 'contract',
      entityId: contractId,
      action: 'contract.sent',
      changes: { signerEmail: [null, signerEmail], signerName: [null, signerName] },
    })

    // operational-audit-trail: status change
    await captureEntityChange({
      tx, tenantId, userId,
      actorName: actorCtx?.actorName ?? null,
      actorEmail: actorCtx?.actorEmail ?? null,
      eventType: 'contract.status_changed',
      entityType: 'contract', entityId: contractId,
      entityLabel: row.title,
      beforeState: { status: 'draft' },
      afterState: { status: 'sent', signerEmail },
      ipAddress: actorCtx?.ipAddress ?? null,
    })

    return serializeRow(row)
  })
}

// ── signContract ──────────────────────────────────────────────────────────────

export async function signContract(
  db: Db,
  tenantId: string,
  contractId: string,
  signatureData: unknown,
): Promise<Contract> {
  return db.transaction(async (tx) => {
    const existing = await tx
      .select()
      .from(contracts)
      .where(and(eq(contracts.tenantId, tenantId), eq(contracts.id, contractId)))
      .limit(1)

    if (existing.length === 0) throw new ContractNotFoundError(contractId)
    if (existing[0]!.status !== 'sent') {
      throw new Error(`Cannot sign contract in status: ${existing[0]!.status}`)
    }

    const now = new Date()
    const [row] = await tx
      .update(contracts)
      .set({ status: 'signed', signatureData, signedAt: now, updatedAt: now })
      .where(and(eq(contracts.tenantId, tenantId), eq(contracts.id, contractId)))
      .returning()

    if (!row) throw new Error('Update failed')

    await tx.insert(auditLog).values({
      tenantId,
      actorId: null,
      actorType: 'system',
      entityType: 'contract',
      entityId: contractId,
      action: 'contract.signed',
      changes: null,
    })

    return serializeRow(row)
  })
}

// ── voidContract ──────────────────────────────────────────────────────────────

export async function voidContract(
  db: Db,
  tenantId: string,
  contractId: string,
  userId: string,
  actorCtx?: ContractActorContext,
): Promise<Contract> {
  return db.transaction(async (tx) => {
    const existing = await tx
      .select()
      .from(contracts)
      .where(and(eq(contracts.tenantId, tenantId), eq(contracts.id, contractId)))
      .limit(1)

    if (existing.length === 0) throw new ContractNotFoundError(contractId)
    if (existing[0]!.status === 'voided') {
      throw new Error('Contract is already voided')
    }

    const now = new Date()
    const [row] = await tx
      .update(contracts)
      .set({ status: 'voided', voidedAt: now, updatedAt: now })
      .where(and(eq(contracts.tenantId, tenantId), eq(contracts.id, contractId)))
      .returning()

    if (!row) throw new Error('Update failed')

    await tx.insert(auditLog).values({
      tenantId,
      actorId: userId,
      actorType: 'user',
      entityType: 'contract',
      entityId: contractId,
      action: 'contract.voided',
      changes: null,
    })

    // operational-audit-trail: status change
    await captureEntityChange({
      tx, tenantId, userId,
      actorName: actorCtx?.actorName ?? null,
      actorEmail: actorCtx?.actorEmail ?? null,
      eventType: 'contract.status_changed',
      entityType: 'contract', entityId: contractId,
      entityLabel: row.title,
      beforeState: { status: existing[0]!.status },
      afterState: { status: 'voided' },
      ipAddress: actorCtx?.ipAddress ?? null,
    })

    return serializeRow(row)
  })
}
