/**
 * Contractor portal API — contractor-portal (wave 8, Task 5).
 *
 *   GET    /contractor-portal/redeem
 *   GET    /contractor-portal/api/me
 *   GET    /contractor-portal/api/time
 *   POST   /contractor-portal/api/time
 *   PATCH  /contractor-portal/api/time/:id
 *   DELETE /contractor-portal/api/time/:id
 *   GET    /contractor-portal/api/time/export.csv
 *   GET    /contractor-portal/api/bills
 */
import { Hono, type Context } from 'hono'
import { setCookie } from 'hono/cookie'
import { z } from 'zod'
import {
  contractorAuthMiddleware,
  timingSafeEqual,
  hashToken,
  signContractorSession,
  CONTRACTOR_COOKIE_NAME,
  CONTRACTOR_COOKIE_OPTS,
  CONTRACTOR_SESSION_TTL_SECONDS,
  type ContractorAuthVariables,
} from '@zync/auth'
import { createDb } from '@zync/db/queries'
import {
  assertContractorProjectAssignment,
  assertTaskBelongsToProject,
  computeContractorEntryTimes,
  createContractorTimeEntry,
  deleteContractorTimeEntry,
  findValidPortalSession,
  getContractorForPortalRedeem,
  getContractorProfile,
  getContractorTimeEntry,
  listContractorPortalBills,
  listContractorPortalTimeEntries,
  buildContractorPortalTimeLogCsvExport,
  contractorPortalCsvHeaders,
  markPortalSessionUsed,
  resolveContractorApprovalRequirement,
  updateContractorTimeEntry,
  ContractorPortalAssignmentError,
  ContractorPortalEntryConflictError,
  ContractorPortalEntryNotFoundError,
  ContractorPortalInactiveError,
} from '@zync/db/queries'
import type { AppEnv, AppVariables } from '../types'
import { createTimeEntrySchema, updateTimeEntrySchema } from '../validation/contractor-portal'

type ContractorPortalEnv = {
  Bindings: AppEnv['Bindings']
  Variables: AppVariables & ContractorAuthVariables
}

const listTimeQuerySchema = z.object({
  month: z.string().regex(/^\d{4}-\d{2}$/).optional(),
  approval_status: z
    .enum(['auto_approved', 'pending', 'approved', 'rejected', 'locked'])
    .optional(),
})

const exportTimeQuerySchema = z.object({
  month: z.string().regex(/^\d{4}-\d{2}$/),
})

/** Map typed contractor-portal query errors to HTTP responses; return null if unhandled. */
function respondContractorPortalError(
  c: Context<ContractorPortalEnv>,
  err: unknown,
  opts: { entryNotFoundStatus: 401 | 404 },
): Response | null {
  if (err instanceof ContractorPortalInactiveError) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
  if (err instanceof ContractorPortalEntryNotFoundError) {
    if (opts.entryNotFoundStatus === 401) {
      return c.json({ error: 'Unauthorized' }, 401)
    }
    return c.json({ error: 'Not found' }, 404)
  }
  if (err instanceof ContractorPortalEntryConflictError) {
    return c.json({ error: err.message }, 409)
  }
  if (err instanceof ContractorPortalAssignmentError) {
    return c.json({ error: err.message }, 400)
  }
  return null
}

export const contractorPortalRoutes = new Hono<ContractorPortalEnv>()

contractorPortalRoutes.get('/redeem', async (c) => {
  const token = c.req.query('token')
  if (!token || token.length < 10) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const tokenHash = await hashToken(token)
  const db = createDb(c.env)
  const portalSession = await findValidPortalSession(db, tokenHash)

  if (!portalSession || !timingSafeEqual(portalSession.tokenHash, tokenHash)) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const contractor = await getContractorForPortalRedeem(
    db,
    portalSession.contractorId,
    portalSession.tenantId,
  )

  if (!contractor || !contractor.active) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const claimed = await markPortalSessionUsed(db, portalSession.id)
  if (!claimed) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const jwt = await signContractorSession(
    {
      sub: portalSession.contractorId,
      tenantId: portalSession.tenantId as import('@zync/types').TenantId,
    },
    c.env.JWT_SECRET,
  )

  setCookie(c, CONTRACTOR_COOKIE_NAME, jwt, {
    ...CONTRACTOR_COOKIE_OPTS,
    maxAge: CONTRACTOR_SESSION_TTL_SECONDS,
  })

  return c.redirect('/contractor-portal/', 302)
})

const contractorPortalApi = new Hono<ContractorPortalEnv>()
contractorPortalApi.use('*', contractorAuthMiddleware)

contractorPortalApi.get('/me', async (c) => {
  const { contractorId, tenantId } = c.get('contractor')
  const db = createDb(c.env)

  try {
    const profile = await getContractorProfile(db, { tenantId, contractorId })
    return c.json(profile)
  } catch (err) {
    const response = respondContractorPortalError(c, err, { entryNotFoundStatus: 401 })
    if (response) return response
    throw err
  }
})

contractorPortalApi.get('/time', async (c) => {
  const { contractorId, tenantId } = c.get('contractor')
  const parsed = listTimeQuerySchema.safeParse(
    Object.fromEntries(new URL(c.req.url).searchParams),
  )
  if (!parsed.success) {
    return c.json({ error: 'Invalid query', issues: parsed.error.issues }, 400)
  }

  const db = createDb(c.env)
  const items = await listContractorPortalTimeEntries(db, {
    tenantId,
    contractorId,
    month: parsed.data.month,
    approvalStatus: parsed.data.approval_status,
  })

  return c.json({ items })
})

contractorPortalApi.get('/time/export.csv', async (c) => {
  const { contractorId, tenantId } = c.get('contractor')
  const parsed = exportTimeQuerySchema.safeParse(
    Object.fromEntries(new URL(c.req.url).searchParams),
  )
  if (!parsed.success) {
    return c.json({ error: 'Invalid query', issues: parsed.error.issues }, 400)
  }

  const db = createDb(c.env)
  const { csv, filename } = await buildContractorPortalTimeLogCsvExport(
    db,
    {
      tenantId,
      contractorId,
      month: parsed.data.month,
    },
    contractorPortalCsvHeaders,
  )

  return new Response(csv, {
    headers: {
      'Content-Type': 'text/csv; charset=utf-8',
      'Content-Disposition': `attachment; filename="${filename}"`,
    },
  })
})

contractorPortalApi.post('/time', async (c) => {
  const { contractorId, tenantId } = c.get('contractor')
  const body = await c.req.json().catch(() => null)
  const parsed = createTimeEntrySchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
  }

  const db = createDb(c.env)

  try {
    await assertContractorProjectAssignment(db, {
      tenantId,
      contractorId,
      projectId: parsed.data.project_id,
    })

    if (parsed.data.task_id) {
      await assertTaskBelongsToProject(db, {
        tenantId,
        projectId: parsed.data.project_id,
        taskId: parsed.data.task_id,
      })
    }

    const requiresApproval = await resolveContractorApprovalRequirement(db, tenantId)
    const approvalStatus = requiresApproval ? 'pending' : 'auto_approved'
    const { startedAt, stoppedAt } = await computeContractorEntryTimes(
      db,
      tenantId,
      parsed.data.date,
      parsed.data.duration_min,
    )

    const created = await createContractorTimeEntry(db, {
      tenantId,
      contractorId,
      input: parsed.data,
      approvalStatus,
      startedAt,
      stoppedAt,
    })

    return c.json(created, 201)
  } catch (err) {
    const response = respondContractorPortalError(c, err, { entryNotFoundStatus: 404 })
    if (response) return response
    throw err
  }
})

contractorPortalApi.patch('/time/:id', async (c) => {
  const { contractorId, tenantId } = c.get('contractor')
  const entryId = c.req.param('id')
  const body = await c.req.json().catch(() => null)
  const parsed = updateTimeEntrySchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
  }

  const db = createDb(c.env)

  try {
    const existing = await getContractorTimeEntry(db, { tenantId, contractorId, entryId })
    if (!existing) {
      return c.json({ error: 'Not found' }, 404)
    }

    const resolvedProjectId = parsed.data.project_id ?? existing.projectId
    if (parsed.data.project_id) {
      await assertContractorProjectAssignment(db, {
        tenantId,
        contractorId,
        projectId: resolvedProjectId,
      })
    }

    const effectiveTaskId =
      parsed.data.task_id !== undefined ? parsed.data.task_id ?? null : existing.taskId

    if (effectiveTaskId) {
      await assertTaskBelongsToProject(db, {
        tenantId,
        projectId: resolvedProjectId,
        taskId: effectiveTaskId,
      })
    }

    let startedAt: Date | undefined
    let stoppedAt: Date | undefined
    if (parsed.data.date !== undefined || parsed.data.duration_min !== undefined) {
      const date = parsed.data.date ?? existing.startedAt.toISOString().slice(0, 10)
      const durationMin =
        parsed.data.duration_min ?? Math.round((existing.durationSeconds ?? 0) / 60)
      const bounds = await computeContractorEntryTimes(db, tenantId, date, durationMin)
      startedAt = bounds.startedAt
      stoppedAt = bounds.stoppedAt
    }

    await updateContractorTimeEntry(db, {
      tenantId,
      contractorId,
      entryId,
      patch: parsed.data,
      startedAt,
      stoppedAt,
    })

    return c.json({ ok: true })
  } catch (err) {
    const response = respondContractorPortalError(c, err, { entryNotFoundStatus: 404 })
    if (response) return response
    throw err
  }
})

contractorPortalApi.delete('/time/:id', async (c) => {
  const { contractorId, tenantId } = c.get('contractor')
  const entryId = c.req.param('id')
  const db = createDb(c.env)

  try {
    await deleteContractorTimeEntry(db, { tenantId, contractorId, entryId })
    return c.json({ ok: true })
  } catch (err) {
    const response = respondContractorPortalError(c, err, { entryNotFoundStatus: 404 })
    if (response) return response
    throw err
  }
})

contractorPortalApi.get('/bills', async (c) => {
  const { contractorId, tenantId } = c.get('contractor')
  const db = createDb(c.env)
  const items = await listContractorPortalBills(db, { tenantId, contractorId })
  return c.json({ items })
})

contractorPortalRoutes.route('/api', contractorPortalApi)
