import type { FastifyInstance, FastifyRequest } from 'fastify'
import bcrypt from 'bcryptjs'
import { randomUUID } from 'node:crypto'
import { z } from 'zod'
import { db, logEvent } from '../db/index.js'

const credentialsSchema = z.object({
  username: z.string().min(3).max(64),
  password: z.string().min(8).max(256),
})

function clientIp(req: FastifyRequest): string {
  const fwd = req.headers['x-forwarded-for']
  if (typeof fwd === 'string' && fwd.length > 0) return fwd.split(',')[0]!.trim()
  return req.ip
}

function clientUserAgent(req: FastifyRequest): string {
  const ua = req.headers['user-agent']
  return (typeof ua === 'string' ? ua : '').slice(0, 512)
}

/** Creates a session row and returns a JWT carrying its id, so the session can be revoked. */
function createSession(app: FastifyInstance, req: FastifyRequest, userId: number, username: string, role: string): string {
  const sid = randomUUID()
  db.prepare('INSERT INTO sessions (id, user_id, user_agent, ip) VALUES (?, ?, ?, ?)').run(
    sid,
    userId,
    clientUserAgent(req),
    clientIp(req),
  )
  return app.jwt.sign({ sub: userId, username, sid, role })
}

export async function authRoutes(app: FastifyInstance) {
  app.get('/api/system/setup-status', async () => {
    const row = db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }
    return { needsSetup: row.count === 0 }
  })

  app.post('/api/setup', async (req, reply) => {
    const existing = db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }
    if (existing.count > 0) {
      return reply.code(409).send({ error: 'Setup already completed' })
    }
    const parsed = credentialsSchema.safeParse(req.body)
    if (!parsed.success) {
      return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
    }
    const { username, password } = parsed.data
    const passwordHash = await bcrypt.hash(password, 12)
    const result = db
      .prepare("INSERT INTO users (username, password_hash, role, active) VALUES (?, ?, 'admin', 1)")
      .run(username, passwordHash)
    const userId = Number(result.lastInsertRowid)
    const token = createSession(app, req, userId, username, 'admin')
    logEvent('account_created', `Account created for ${username}`)
    return { token }
  })

  app.post('/api/auth/login', async (req, reply) => {
    const parsed = credentialsSchema.safeParse(req.body)
    if (!parsed.success) {
      return reply.code(400).send({ error: 'Invalid input' })
    }
    const { username, password } = parsed.data
    const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username) as
      | { id: number; username: string; password_hash: string; role: string; active: number }
      | undefined
    const passwordOk = !!user && (await bcrypt.compare(password, user.password_hash))
    const ok = passwordOk && user.active === 1
    db.prepare('INSERT INTO login_events (user_id, username, ip, user_agent, success) VALUES (?, ?, ?, ?, ?)').run(
      user?.id ?? null,
      username,
      clientIp(req),
      clientUserAgent(req),
      ok ? 1 : 0,
    )
    if (!user || !passwordOk) {
      return reply.code(401).send({ error: 'Invalid username or password' })
    }
    if (user.active !== 1) {
      return reply.code(403).send({ error: 'This account has been deactivated' })
    }
    const token = createSession(app, req, user.id, user.username, user.role)
    logEvent('user_login', `${user.username} logged in`)
    return { token }
  })

  app.get('/api/auth/me', { onRequest: [app.authenticate] }, async (req) => {
    const payload = req.user as { sub: number; username: string }
    const user = db
      .prepare('SELECT id, username, display_name, email, avatar_data_url, role, active FROM users WHERE id = ?')
      .get(payload.sub)
    return { user }
  })

  const profileUpdateSchema = z.object({
    displayName: z.string().max(128).nullable().optional(),
    email: z.string().email().max(256).nullable().optional(),
    avatarDataUrl: z.string().max(2_000_000).nullable().optional(),
  })

  app.put('/api/auth/me', { onRequest: [app.authenticate] }, async (req, reply) => {
    const payload = req.user as { sub: number; username: string }
    const parsed = profileUpdateSchema.safeParse(req.body)
    if (!parsed.success) {
      return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
    }
    const { displayName, email, avatarDataUrl } = parsed.data
    if (displayName !== undefined) db.prepare('UPDATE users SET display_name = ? WHERE id = ?').run(displayName, payload.sub)
    if (email !== undefined) db.prepare('UPDATE users SET email = ? WHERE id = ?').run(email, payload.sub)
    if (avatarDataUrl !== undefined) db.prepare('UPDATE users SET avatar_data_url = ? WHERE id = ?').run(avatarDataUrl, payload.sub)
    const user = db
      .prepare('SELECT id, username, display_name, email, avatar_data_url, role, active FROM users WHERE id = ?')
      .get(payload.sub)
    return { user }
  })

  const passwordChangeSchema = z.object({
    currentPassword: z.string().min(1).max(256),
    newPassword: z.string().min(8).max(256),
  })

  app.put('/api/auth/password', { onRequest: [app.authenticate] }, async (req, reply) => {
    const payload = req.user as { sub: number; username: string; sid?: string }
    const parsed = passwordChangeSchema.safeParse(req.body)
    if (!parsed.success) {
      return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
    }
    const { currentPassword, newPassword } = parsed.data
    const user = db.prepare('SELECT id, username, password_hash FROM users WHERE id = ?').get(payload.sub) as
      | { id: number; username: string; password_hash: string }
      | undefined
    if (!user || !(await bcrypt.compare(currentPassword, user.password_hash))) {
      return reply.code(401).send({ error: 'Current password is incorrect' })
    }
    const passwordHash = await bcrypt.hash(newPassword, 12)
    db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(passwordHash, user.id)
    // Revoke every other session — a password change should sign out other devices,
    // but keep the current session alive so the user isn't logged out of this tab.
    if (payload.sid) {
      db.prepare('DELETE FROM sessions WHERE user_id = ? AND id != ?').run(user.id, payload.sid)
    } else {
      db.prepare('DELETE FROM sessions WHERE user_id = ?').run(user.id)
    }
    logEvent('password_changed', `${user.username} changed their password`)
    return { ok: true }
  })

  app.get('/api/auth/sessions', { onRequest: [app.authenticate] }, async (req) => {
    const payload = req.user as { sub: number; sid?: string }
    const rows = db
      .prepare('SELECT id, user_agent, ip, created_at, last_seen_at FROM sessions WHERE user_id = ? ORDER BY last_seen_at DESC')
      .all(payload.sub) as Array<{ id: string; user_agent: string | null; ip: string | null; created_at: string; last_seen_at: string }>
    return {
      sessions: rows.map((r) => ({
        id: r.id,
        userAgent: r.user_agent,
        ip: r.ip,
        createdAt: r.created_at,
        lastSeenAt: r.last_seen_at,
        current: r.id === payload.sid,
      })),
    }
  })

  app.delete('/api/auth/sessions/:id', { onRequest: [app.authenticate] }, async (req, reply) => {
    const payload = req.user as { sub: number; sid?: string }
    const id = (req.params as { id: string }).id
    if (id === payload.sid) {
      return reply.code(400).send({ error: 'Cannot revoke the current session; use sign out instead' })
    }
    db.prepare('DELETE FROM sessions WHERE id = ? AND user_id = ?').run(id, payload.sub)
    return { ok: true }
  })

  app.post('/api/auth/logout', { onRequest: [app.authenticate] }, async (req) => {
    const payload = req.user as { sub: number; sid?: string }
    if (payload.sid) db.prepare('DELETE FROM sessions WHERE id = ? AND user_id = ?').run(payload.sid, payload.sub)
    return { ok: true }
  })

  app.get('/api/auth/login-events', { onRequest: [app.authenticate] }, async (req) => {
    const limitRaw = Number((req.query as { limit?: string }).limit)
    const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(limitRaw, 1), 100) : 20
    const rows = db
      .prepare('SELECT id, username, ip, user_agent, success, created_at FROM login_events ORDER BY created_at DESC LIMIT ?')
      .all(limit) as Array<{ id: number; username: string | null; ip: string | null; user_agent: string | null; success: number; created_at: string }>
    return {
      events: rows.map((r) => ({
        id: r.id,
        username: r.username,
        ip: r.ip,
        userAgent: r.user_agent,
        success: r.success === 1,
        createdAt: r.created_at,
      })),
    }
  })

  // --- User management (admin-only) ---

  const MAX_USERS = 10

  app.get('/api/users', { onRequest: [app.requireAdmin] }, async () => {
    const rows = db
      .prepare('SELECT id, username, display_name, email, role, active, created_at FROM users ORDER BY created_at ASC')
      .all() as Array<{
      id: number
      username: string
      display_name: string | null
      email: string | null
      role: string
      active: number
      created_at: string
    }>
    return {
      users: rows.map((r) => ({
        id: r.id,
        username: r.username,
        displayName: r.display_name,
        email: r.email,
        role: r.role,
        active: r.active === 1,
        createdAt: r.created_at,
      })),
    }
  })

  const createUserSchema = z.object({
    username: z.string().min(3).max(64),
    password: z.string().min(8).max(256),
    role: z.enum(['admin', 'member']).default('member'),
    displayName: z.string().max(128).nullable().optional(),
    email: z.string().email().max(256).nullable().optional(),
  })

  app.post('/api/users', { onRequest: [app.requireAdmin] }, async (req, reply) => {
    const parsed = createUserSchema.safeParse(req.body)
    if (!parsed.success) {
      return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
    }
    const { username, password, role, displayName, email } = parsed.data
    const count = (db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }).count
    if (count >= MAX_USERS) {
      return reply.code(409).send({ error: `User limit reached (${MAX_USERS} maximum)` })
    }
    const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username)
    if (existing) {
      return reply.code(409).send({ error: 'Username already taken' })
    }
    const passwordHash = await bcrypt.hash(password, 12)
    const result = db
      .prepare('INSERT INTO users (username, password_hash, role, active, display_name, email) VALUES (?, ?, ?, 1, ?, ?)')
      .run(username, passwordHash, role, displayName ?? null, email ?? null)
    logEvent('user_created', `User ${username} created (${role})`)
    return { user: { id: Number(result.lastInsertRowid), username, role, active: true } }
  })

  const updateUserSchema = z.object({
    role: z.enum(['admin', 'member']).optional(),
    active: z.boolean().optional(),
  })

  app.put('/api/users/:id', { onRequest: [app.requireAdmin] }, async (req, reply) => {
    const actor = req.user as { sub: number }
    const id = Number((req.params as { id: string }).id)
    const parsed = updateUserSchema.safeParse(req.body)
    if (!parsed.success) {
      return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
    }
    const target = db.prepare('SELECT id, username, role, active FROM users WHERE id = ?').get(id) as
      | { id: number; username: string; role: string; active: number }
      | undefined
    if (!target) return reply.code(404).send({ error: 'User not found' })

    const { role, active } = parsed.data
    // Guard rails: don't let an admin lock everyone out by demoting/deactivating the
    // last remaining active admin (including themselves).
    const willDemote = role === 'member' && target.role === 'admin'
    const willDeactivate = active === false && target.active === 1
    if (willDemote || willDeactivate) {
      const otherActiveAdmins = (
        db
          .prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin' AND active = 1 AND id != ?")
          .get(id) as { count: number }
      ).count
      if (otherActiveAdmins === 0) {
        return reply.code(409).send({ error: 'Cannot remove the last active admin' })
      }
    }

    if (role !== undefined) db.prepare('UPDATE users SET role = ? WHERE id = ?').run(role, id)
    if (active !== undefined) {
      db.prepare('UPDATE users SET active = ? WHERE id = ?').run(active ? 1 : 0, id)
      // Deactivating a user kills their sessions immediately.
      if (!active) db.prepare('DELETE FROM sessions WHERE user_id = ?').run(id)
    }
    logEvent('user_updated', `User ${target.username} updated by admin`)
    void actor
    const updated = db
      .prepare('SELECT id, username, display_name, email, role, active, created_at FROM users WHERE id = ?')
      .get(id) as { id: number; username: string; display_name: string | null; email: string | null; role: string; active: number; created_at: string }
    return {
      user: {
        id: updated.id,
        username: updated.username,
        displayName: updated.display_name,
        email: updated.email,
        role: updated.role,
        active: updated.active === 1,
        createdAt: updated.created_at,
      },
    }
  })

  app.delete('/api/users/:id', { onRequest: [app.requireAdmin] }, async (req, reply) => {
    const actor = req.user as { sub: number }
    const id = Number((req.params as { id: string }).id)
    if (id === actor.sub) {
      return reply.code(400).send({ error: 'You cannot delete your own account' })
    }
    const target = db.prepare('SELECT id, username, role, active FROM users WHERE id = ?').get(id) as
      | { id: number; username: string; role: string; active: number }
      | undefined
    if (!target) return reply.code(404).send({ error: 'User not found' })
    if (target.role === 'admin' && target.active === 1) {
      const otherActiveAdmins = (
        db
          .prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin' AND active = 1 AND id != ?")
          .get(id) as { count: number }
      ).count
      if (otherActiveAdmins === 0) {
        return reply.code(409).send({ error: 'Cannot delete the last active admin' })
      }
    }
    db.prepare('DELETE FROM sessions WHERE user_id = ?').run(id)
    db.prepare('DELETE FROM users WHERE id = ?').run(id)
    logEvent('user_deleted', `User ${target.username} deleted by admin`)
    return { ok: true }
  })
}
