/**
 * Contracts routes — contracts-esignature (wave-8 leaf 4).
 * Mounted at /api/contracts in apps/zync-api/src/routes/index.ts.
 *
 * GET    /                            list contracts (optional ?status filter)
 * POST   /                            create contract
 * GET    /:id                         get contract detail
 * POST   /:id/send                    send for signature
 * POST   /:id/void                    void contract
 * POST   /:id/generate-invoice        generate draft invoice from signed contract (wave-9 leaf2)
 *
 * wave-12: multi-signatory-coordination
 * GET    /:id/signatories             list signatories with derived status + reminder counts
 * PATCH  /:id/signatories             bulk upsert signatories (DRAFT only)
 * POST   /:id/signatories/:sigId/reminder  send manual reminder (24h-gated)
 * POST   /:id/signatories/:sigId/resend    regenerate token + resend (OWNER/ADMIN only)
 * PATCH  /:id/signatories/:sigId          replace name/email on unsigned signatory (SENT/VIEWED)
 *
 * wave-12: contract-renewal-amendment
 * POST   /:id/renew              create renewal DRAFT from SIGNED contract
 * POST   /:id/amend              create amendment DRAFT from SIGNED contract
 * GET    /:id/lineage            get renewal/amendment lineage chain
 */
import { Hono } from 'hono'
import { z } from 'zod'
import {
  listContracts,
  getContract,
  createContract,
  sendContract,
  voidContract,
  ContractNotFoundError,
  generateInvoiceFromContract,
  renewContract,
  amendContract,
  getContractLineage,
  getContractTenantScoped,
  listSignatories,
  getSignatory,
  checkSignatoryEmailCollision,
  bulkUpsertSignatories,
  markManualReminderSent,
  resendSignatoryLink,
  replaceSignatory,
  assertTenantOwnsCustomer,
  invalidTenantReferenceBody,
} from '@zync/db/queries'
import type { ContractSignatoryRow } from '@zync/db/queries'
import { sendEmail } from '@zync/notifications'
import { isAtLeastRole } from '@zync/auth'
import type { AppEnv } from '../../types'
import { authMiddleware } from '../../middleware/auth'
import { requirePermission } from '../../middleware/guards'
import {
  isSequential,
  currentTurnOrder,
  assertOrdersValid,
} from '../../contracts/signing-order'

// ── Zod schemas ───────────────────────────────────────────────────────────────

const createContractSchema = z.object({
  title: z.string().min(1).max(500),
  content: z.string().min(1),
  customerId: z.string().uuid().optional().nullable(),
})

const sendContractSchema = z.object({
  signerEmail: z.string().email(),
  signerName: z.string().min(1).max(255),
})

// wave-12: signatories schemas
const bulkSignatoriesSchema = z.object({
  signatories: z
    .array(
      z.object({
        id: z.string().uuid().optional(),
        name: z.string().min(1).max(200),
        email: z.string().email().max(320),
        order: z.number().int().min(1).max(3),
      }),
    )
    .min(1)
    .max(3),
})

const replaceSignatorySchema = z
  .object({
    name: z.string().min(1).max(200).optional(),
    email: z.string().email().max(320).optional(),
  })
  .refine((v) => v.name !== undefined || v.email !== undefined, {
    message: 'at least one field (name or email) is required',
  })

// wave-12: contract-renewal-amendment schemas
const renewContractSchema = z.object({
  title: z.string().min(1).max(500),
  effectiveDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
  expiryDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
})

const amendContractSchema = z.object({
  title: z.string().min(1).max(500),
  content: z.string().min(1),
  effectiveDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
})

// ── Types ─────────────────────────────────────────────────────────────────────

export type SignatoryStatus = 'signed' | 'viewed' | 'sent' | 'waiting' | 'declined'

interface SerializedSignatory {
  id: string
  name: string | null
  email: string
  order: number
  status: SignatoryStatus
  signedAt: string | null
  viewedAt: string | null
  declinedAt: string | null
  reminderSentAt: string | null
  reminderCount: number
  waitingFor: string | null
}

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

function serializeSignatory(
  sig: ContractSignatoryRow,
  all: ContractSignatoryRow[],
): SerializedSignatory {
  const allOrderRows = all.map((s) => ({
    id: s.id,
    order: s.order,
    signedAt: s.signedAt,
    declinedAt: s.declinedAt,
  }))

  let status: SignatoryStatus
  if (sig.declinedAt !== null) {
    status = 'declined'
  } else if (sig.signedAt !== null) {
    status = 'signed'
  } else if (isSequential(allOrderRows) && sig.order !== currentTurnOrder(allOrderRows)) {
    status = 'waiting'
  } else {
    status = 'sent'
  }

  // For 'waiting': find who is currently blocking (lowest-order unsigned/undeclined)
  let waitingFor: string | null = null
  if (status === 'waiting') {
    const blocker = all.find(
      (s) =>
        s.order === currentTurnOrder(allOrderRows) && s.signedAt === null && s.declinedAt === null,
    )
    waitingFor = blocker?.name ?? blocker?.email ?? null
  }

  return {
    id: sig.id,
    name: sig.name ?? null,
    email: sig.email,
    order: sig.order,
    status,
    signedAt: sig.signedAt ? sig.signedAt.toISOString() : null,
    viewedAt: null, // viewedAt not stored per-signatory in this schema version
    declinedAt: sig.declinedAt ? sig.declinedAt.toISOString() : null,
    reminderSentAt: sig.reminderSentAt ? sig.reminderSentAt.toISOString() : null,
    reminderCount: sig.reminderCount,
    waitingFor,
  }
}

// ── Router (authenticated routes) ────────────────────────────────────────────

const contractsRoute = new Hono<AppEnv>()

// Auth guard on all except /sign
contractsRoute.use('/', authMiddleware)
contractsRoute.use('/:id', authMiddleware)
contractsRoute.use('/:id/send', authMiddleware)
contractsRoute.use('/:id/void', authMiddleware)
contractsRoute.use('/:id/generate-invoice', authMiddleware)
contractsRoute.use('/:id/signatories', authMiddleware)
contractsRoute.use('/:id/signatories/*', authMiddleware)
contractsRoute.use('/:id/renew', authMiddleware)
contractsRoute.use('/:id/amend', authMiddleware)
contractsRoute.use('/:id/lineage', authMiddleware)

// GET /api/contracts
contractsRoute.get('/', requirePermission('contracts:read'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const status = new URL(c.req.url).searchParams.get('status') ?? undefined
  const db = c.get('db')
  const result = await listContracts(db, session.tid, status ? { status } : undefined)
  return c.json(result, 200)
})

// POST /api/contracts
contractsRoute.post('/', requirePermission('contracts:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const parsed = createContractSchema.safeParse(await c.req.json())
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', issues: parsed.error.issues }, 400)
  }

  const db = c.get('db')
  if (!(await assertTenantOwnsCustomer(db, session.tid, parsed.data.customerId))) {
    return c.json(invalidTenantReferenceBody('customerId'), 400)
  }
  const contract = await createContract(db, session.tid, session.sub, parsed.data)
  return c.json(contract, 201)
})

// GET /api/contracts/:id
contractsRoute.get('/:id', requirePermission('contracts:read'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const db = c.get('db')
  try {
    const contract = await getContract(db, session.tid, c.req.param('id'))
    return c.json(contract, 200)
  } catch (err) {
    if (err instanceof ContractNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

// POST /api/contracts/:id/send
contractsRoute.post('/:id/send', requirePermission('contracts:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const parsed = sendContractSchema.safeParse(await c.req.json())
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', issues: parsed.error.issues }, 400)
  }

  const db = c.get('db')
  try {
    const contract = await sendContract(
      db,
      session.tid,
      c.req.param('id'),
      parsed.data.signerEmail,
      parsed.data.signerName,
      session.sub,
    )
    return c.json(contract, 200)
  } catch (err) {
    if (err instanceof ContractNotFoundError) return c.json({ error: 'Not found' }, 404)
    if (err instanceof Error && err.message.startsWith('Cannot send')) {
      return c.json({ error: err.message }, 409)
    }
    throw err
  }
})

// POST /api/contracts/:id/void
contractsRoute.post('/:id/void', requirePermission('contracts:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const db = c.get('db')
  try {
    const contract = await voidContract(db, session.tid, c.req.param('id'), session.sub)
    return c.json(contract, 200)
  } catch (err) {
    if (err instanceof ContractNotFoundError) return c.json({ error: 'Not found' }, 404)
    if (err instanceof Error && err.message === 'Contract is already voided') {
      return c.json({ error: err.message }, 409)
    }
    throw err
  }
})

// POST /api/contracts/:id/generate-invoice
contractsRoute.post('/:id/generate-invoice', requirePermission('contracts:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  // Also requires invoices:write since this creates an invoice
  if (!session.permissions?.includes('invoices:write')) {
    return c.json({ error: 'invoices:write permission required' }, 403)
  }

  const db = c.get('db')
  try {
    const invoice = await generateInvoiceFromContract(
      db,
      session.tid,
      session.sub,
      c.req.param('id'),
    )
    return c.json(invoice, 201)
  } catch (err) {
    if (err instanceof ContractNotFoundError) return c.json({ error: 'Not found' }, 404)
    if (err instanceof Error) {
      const msg = err.message
      if (
        msg === 'Contract must be signed to generate an invoice' ||
        msg === 'Contract has no customer — add a customer before generating an invoice' ||
        msg.startsWith('Invoice already exists:')
      ) {
        return c.json({ error: msg }, 409)
      }
    }
    throw err
  }
})

// ── wave-12: Signatory management routes ─────────────────────────────────────

// GET /api/contracts/:id/signatories
contractsRoute.get('/:id/signatories', requirePermission('contracts:read'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const db = c.get('db')
  const contractId = c.req.param('id')

  const contract = await getContractTenantScoped(db, contractId, session.tid)
  if (!contract) return c.json({ error: 'Not found' }, 404)

  const rows = await listSignatories(db, contractId, session.tid)
  const serialized = rows.map((row) => serializeSignatory(row, rows))
  return c.json({ signatories: serialized }, 200)
})

// PATCH /api/contracts/:id/signatories — bulk upsert (DRAFT only)
contractsRoute.patch('/:id/signatories', requirePermission('contracts:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const db = c.get('db')
  const contractId = c.req.param('id')

  const contract = await getContractTenantScoped(db, contractId, session.tid)
  if (!contract) return c.json({ error: 'Not found' }, 404)

  // Only allowed in DRAFT status
  if (contract.status !== 'draft') {
    return c.json(
      {
        error:
          'Signing order is locked after the contract is sent. Void and re-send to change order.',
      },
      422,
    )
  }

  const parsed = bulkSignatoriesSchema.safeParse(await c.req.json())
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', issues: parsed.error.issues }, 400)
  }

  const { signatories } = parsed.data

  // Validate order values
  try {
    assertOrdersValid(signatories.map((s) => s.order))
  } catch {
    return c.json(
      {
        error:
          'Signing orders must be unique per contract, or all set to 1 for simultaneous signing',
      },
      422,
    )
  }

  // Check for duplicate emails in payload
  const emails = signatories.map((s) => s.email.toLowerCase())
  if (new Set(emails).size !== emails.length) {
    return c.json({ error: 'Duplicate email in signatories payload' }, 422)
  }

  const result = await bulkUpsertSignatories(db, contractId, session.tid, session.sub, signatories)

  const serialized = result.map((row) => serializeSignatory(row, result))
  return c.json({ signatories: serialized }, 200)
})

// POST /api/contracts/:id/signatories/:sigId/reminder — 24h-gated manual reminder
contractsRoute.post(
  '/:id/signatories/:sigId/reminder',
  requirePermission('contracts:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const db = c.get('db')
    const contractId = c.req.param('id')
    const sigId = c.req.param('sigId')

    const contract = await getContractTenantScoped(db, contractId, session.tid)
    if (!contract) return c.json({ error: 'Not found' }, 404)

    const signatory = await getSignatory(db, sigId, contractId, session.tid)
    if (!signatory) return c.json({ error: 'Not found' }, 404)

    // Reject if already signed or declined
    if (signatory.signedAt !== null) {
      return c.json({ error: 'Signatory has already signed' }, 422)
    }
    if (signatory.declinedAt !== null) {
      return c.json({ error: 'Signatory has declined' }, 422)
    }

    // 24h app-level rate limit (DB-column check — NOT a CF RateLimiter binding)
    if (signatory.reminderSentAt !== null) {
      const hoursSince =
        (Date.now() - signatory.reminderSentAt.getTime()) / (1000 * 60 * 60)
      if (hoursSince < 24) {
        const retryAfterSeconds = Math.ceil((24 - hoursSince) * 3600)
        return c.json(
          { error: 'Reminder already sent in the last 24 hours', retryAfter: retryAfterSeconds },
          429,
        )
      }
    }

    // Send reminder email via @zync/notifications sendEmail
    await sendEmail(
      {
        to: signatory.email,
        templateKey: 'invitation',
        locale: 'he-IL',
        vars: {
          subject: `Reminder: Please sign "${contract.title}"`,
          title: `Reminder: Please sign "${contract.title}"`,
          body: `Hi ${signatory.name ?? signatory.email}, this is a reminder to sign the contract "${contract.title}".`,
          inviteUrl: `https://app.zync.is/sign/${signatory.token}`,
          ctaLabel: 'Sign contract',
        },
      },
      c.env,
    )

    const now = new Date()

    const updated = await markManualReminderSent(
      db,
      sigId,
      contractId,
      session.tid,
      session.sub,
      now,
    )

    return c.json(
      {
        reminderSentAt: updated?.reminderSentAt?.toISOString() ?? now.toISOString(),
        reminderCount: updated?.reminderCount ?? signatory.reminderCount + 1,
      },
      200,
    )
  },
)

// POST /api/contracts/:id/signatories/:sigId/resend — regen token (OWNER/ADMIN only)
contractsRoute.post(
  '/:id/signatories/:sigId/resend',
  requirePermission('contracts:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    // Resend requires OWNER or ADMIN role (stricter than contracts:write)
    if (!isAtLeastRole(session.role, 'ADMIN')) {
      return c.json({ error: 'OWNER or ADMIN role required to resend signing links' }, 403)
    }

    const db = c.get('db')
    const contractId = c.req.param('id')
    const sigId = c.req.param('sigId')

    const contract = await getContractTenantScoped(db, contractId, session.tid)
    if (!contract) return c.json({ error: 'Not found' }, 404)

    const signatory = await getSignatory(db, sigId, contractId, session.tid)
    if (!signatory) return c.json({ error: 'Not found' }, 404)

    if (signatory.signedAt !== null) {
      return c.json({ error: 'Signatory has already signed — cannot resend link' }, 422)
    }

    const newToken = crypto.randomUUID()
    const newExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days

    await resendSignatoryLink(db, sigId, contractId, session.tid, session.sub, newToken, newExpiry)

    // Send signing invitation with new token
    await sendEmail(
      {
        to: signatory.email,
        templateKey: 'invitation',
        locale: 'he-IL',
        vars: {
          subject: `New signing link: "${contract.title}"`,
          title: `New signing link: "${contract.title}"`,
          body: `Hi ${signatory.name ?? signatory.email}, here is your new signing link for "${contract.title}".`,
          inviteUrl: `https://app.zync.is/sign/${newToken}`,
          ctaLabel: 'Sign contract',
        },
      },
      c.env,
    )

    return c.json({ ok: true }, 200)
  },
)

// PATCH /api/contracts/:id/signatories/:sigId — replace name/email (SENT, not yet signed)
contractsRoute.patch(
  '/:id/signatories/:sigId',
  requirePermission('contracts:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const db = c.get('db')
    const contractId = c.req.param('id')
    const sigId = c.req.param('sigId')

    const contract = await getContractTenantScoped(db, contractId, session.tid)
    if (!contract) return c.json({ error: 'Not found' }, 404)

    // Only allowed on SENT or VIEWED contracts
    if (contract.status !== 'sent' && contract.status !== 'viewed') {
      return c.json(
        { error: 'Signatory replacement only allowed on contracts in SENT or VIEWED status' },
        422,
      )
    }

    const signatory = await getSignatory(db, sigId, contractId, session.tid)
    if (!signatory) return c.json({ error: 'Not found' }, 404)

    if (signatory.signedAt !== null) {
      return c.json({ error: 'Cannot replace a signatory who has already signed' }, 422)
    }

    const parsed = replaceSignatorySchema.safeParse(await c.req.json())
    if (!parsed.success) {
      return c.json({ error: 'Invalid request', issues: parsed.error.issues }, 400)
    }

    const newEmail = parsed.data.email ?? signatory.email
    const newName = parsed.data.name ?? signatory.name

    // Check UNIQUE(contract_id, email) collision if email changed
    if (parsed.data.email && parsed.data.email.toLowerCase() !== signatory.email.toLowerCase()) {
      const collision = await checkSignatoryEmailCollision(db, contractId, newEmail)
      if (collision) {
        return c.json({ error: 'Email already exists for another signatory on this contract' }, 422)
      }
    }

    const newToken = crypto.randomUUID()
    const newExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days

    await replaceSignatory(
      db,
      sigId,
      contractId,
      session.tid,
      session.sub,
      signatory.email,
      newEmail,
      newName,
      newToken,
      newExpiry,
    )

    // Send signing invitation to new email
    await sendEmail(
      {
        to: newEmail,
        templateKey: 'invitation',
        locale: 'he-IL',
        vars: {
          subject: `Please sign: "${contract.title}"`,
          title: `Please sign: "${contract.title}"`,
          body: `Hi ${newName ?? newEmail}, you have been invited to sign the contract "${contract.title}".`,
          inviteUrl: `https://app.zync.is/sign/${newToken}`,
          ctaLabel: 'Sign contract',
        },
      },
      c.env,
    )

    return c.json({ ok: true }, 200)
  },
)

// ── wave-12: contract-renewal-amendment ──────────────────────────────────────

// POST /api/contracts/:id/renew
contractsRoute.post('/:id/renew', requirePermission('contracts:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const parsed = renewContractSchema.safeParse(await c.req.json())
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', issues: parsed.error.issues }, 400)
  }

  const db = c.get('db')
  try {
    const contract = await renewContract(db, session.tid, c.req.param('id'), session.sub, parsed.data)
    return c.json(contract, 201)
  } catch (err) {
    if (err instanceof Error && err.message.startsWith('Contract not found')) {
      return c.json({ error: 'Not found' }, 404)
    }
    if (err instanceof Error && err.message.startsWith('Cannot renew')) {
      return c.json({ error: err.message }, 409)
    }
    throw err
  }
})

// POST /api/contracts/:id/amend
contractsRoute.post('/:id/amend', requirePermission('contracts:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const parsed = amendContractSchema.safeParse(await c.req.json())
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', issues: parsed.error.issues }, 400)
  }

  const db = c.get('db')
  try {
    const contract = await amendContract(db, session.tid, c.req.param('id'), session.sub, parsed.data)
    return c.json(contract, 201)
  } catch (err) {
    if (err instanceof Error && err.message.startsWith('Contract not found')) {
      return c.json({ error: 'Not found' }, 404)
    }
    if (err instanceof Error && err.message.startsWith('Cannot amend')) {
      return c.json({ error: err.message }, 409)
    }
    throw err
  }
})

// GET /api/contracts/:id/lineage
contractsRoute.get('/:id/lineage', requirePermission('contracts:read'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const db = c.get('db')
  try {
    const lineage = await getContractLineage(db, session.tid, c.req.param('id'))
    return c.json(lineage, 200)
  } catch (err) {
    if (err instanceof Error && err.message.startsWith('Contract not found')) {
      return c.json({ error: 'Not found' }, 404)
    }
    throw err
  }
})

export { contractsRoute }
