/**
 * Project CRUD routes — projects module.
 * Mounted at /api/projects (behind authMiddleware + requireModuleEnabled in router).
 *
 * GET    /             list projects (cursor-paginated, filterable, ≤100)
 * POST   /             create project
 * GET    /:id          get project with stats
 * PATCH  /:id          update project
 * DELETE /:id          archive project (soft delete; returns 204)
 */
import { Hono } from 'hono'
import {
  listProjects,
  getProjectWithStats,
  createProject,
  updateProject,
  archiveProjectLifecycle,
  assertActiveTenantAssignee,
  assertTenantOwnsCustomer,
  invalidTenantReferenceBody,
} from '@zync/db/queries'
import type { AppEnv } from '../../types'
import { requirePermission } from '../../middleware/guards'
import {
  createProjectSchema,
  updateProjectSchema,
  listProjectsQuerySchema,
} from '../../schemas/projects'
import type { ListProjectsParams } from '@zync/types'
import {
  applyFieldPermissions,
  applyFieldPermissionsToPage,
  attachReadOnlyMeta,
  getFieldPermissionContext,
} from '../field-permissions/enforcement'

export const projectCrudRoute = new Hono<AppEnv>()

function hasTenantWideProjectVisibility(role: string | null | undefined): boolean {
  return role === 'OWNER' || role === 'ADMIN' || role === 'VIEWER'
}

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

  const url = new URL(c.req.url)
  const rawQuery = Object.fromEntries(url.searchParams.entries())
  const parsed = listProjectsQuerySchema.safeParse(rawQuery)
  if (!parsed.success) {
    return c.json({ error: 'Invalid query', issues: parsed.error.issues }, 400)
  }

  const params: ListProjectsParams = {
    cursor: parsed.data.cursor,
    limit: parsed.data.limit,
    status: parsed.data.status ?? ['active', 'completed'],
    billing_type: parsed.data.billing_type,
    customer_id: parsed.data.customer_id,
    sort: parsed.data.sort,
  }

  // fullVisibility: true if the session role has tenant-wide projects:read
  // The session's permissions array is set by authMiddleware from RBAC.
  // session is narrowed to a user SessionPayload above, so .permissions is typed.
  const fullVisibility = hasTenantWideProjectVisibility(session.role)

  const db = c.get('db')
  let page
  try {
    page = await listProjects(db, session.tid, params, {
      fullVisibility,
      userId: session.sub,
    })
  } catch (err) {
    console.error('listProjects error:', err)
    throw err
  }

  const { role, rules } = await getFieldPermissionContext(db, session)
  return c.json(applyFieldPermissionsToPage(page, 'project', role, rules), 200)
})

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

  const parsed = createProjectSchema.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.customer_id))) {
    return c.json(invalidTenantReferenceBody('customer_id'), 400)
  }

  if (parsed.data.members && parsed.data.members.length > 0) {
    for (const m of parsed.data.members) {
      if (!(await assertActiveTenantAssignee(db, session.tid, m.user_id))) {
        return c.json(invalidTenantReferenceBody('members'), 400)
      }
    }
  }

  const project = await createProject(
    db,
    session.tid,
    parsed.data as Parameters<typeof createProject>[2],
    session.sub,
    c.env.QUEUE,
  )
  return c.json(project, 201)
})

// GET /api/projects/:id
projectCrudRoute.get('/:id', requirePermission('projects: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 access = { fullVisibility: hasTenantWideProjectVisibility(session.role), userId: session.sub }
  const result = await getProjectWithStats(db, session.tid, c.req.param('id'), access)
  if (!result) return c.json({ error: 'Not found' }, 404)
  const { role, rules } = await getFieldPermissionContext(db, session)
  const filtered = applyFieldPermissions(result, 'project', role, rules)
  return c.json(attachReadOnlyMeta(filtered.data, filtered.readOnly), 200)
})

// PATCH /api/projects/:id
projectCrudRoute.patch('/:id', requirePermission('projects:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const parsed = updateProjectSchema.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 (parsed.data.customer_id !== undefined) {
    if (!(await assertTenantOwnsCustomer(db, session.tid, parsed.data.customer_id))) {
      return c.json(invalidTenantReferenceBody('customer_id'), 400)
    }
  }

  try {
    const project = await updateProject(
      db,
      session.tid,
      c.req.param('id'),
      parsed.data,
      session.sub,
      { fullVisibility: hasTenantWideProjectVisibility(session.role), userId: session.sub },
    )
    return c.json(project, 200)
  } catch (err) {
    if (err instanceof Error && err.message === 'Project not found') {
      return c.json({ error: 'Not found' }, 404)
    }
    throw err
  }
})

// DELETE /api/projects/:id delegates to the lifecycle archive rule.
projectCrudRoute.delete('/:id', requirePermission('projects:delete'), 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 project = await getProjectWithStats(db, session.tid, c.req.param('id'), {
      fullVisibility: hasTenantWideProjectVisibility(session.role),
      userId: session.sub,
    })
    if (!project) return c.json({ error: 'Not found' }, 404)
    if (project.status !== 'completed') {
      return c.json(
        {
          error: 'Project must be completed before archiving',
          code: 'INVALID_TRANSITION',
        },
        409,
      )
    }
    const result = await archiveProjectLifecycle(db, session.tid, c.req.param('id'), session.sub)
    if (!result.ok) {
      return c.json({ error: result.error?.message, code: result.error?.code }, result.error?.code === 'NOT_FOUND' ? 404 : 409)
    }
    return new Response(null, { status: 204 })
  } catch (err) {
    if (err instanceof Error && err.message === 'Project not found') {
      return c.json({ error: 'Not found' }, 404)
    }
    throw err
  }
})
