import crypto from 'node:crypto'
import bcrypt from 'bcrypt'
import jwt from 'jsonwebtoken'
import { getDb } from '../db/client.js'

const sql = getDb()

const REFRESH_HASH_COST = 10

const getJwtSecret = () => {
  const secret = process.env.AUTH_JWT_SECRET
  if (!secret || secret.length < 32) {
    throw new Error('AUTH_JWT_SECRET is missing or too short (min 32 chars)')
  }
  return secret
}

const getAccessTokenTtlSeconds = () => {
  const raw = process.env.ACCESS_TOKEN_TTL_SECONDS
  const n = raw ? parseInt(raw, 10) : 3600
  return Number.isFinite(n) && n > 0 ? n : 3600
}

const getRefreshTokenTtlDays = () => {
  const raw = process.env.REFRESH_TOKEN_TTL_DAYS
  const n = raw ? parseInt(raw, 10) : 30
  return Number.isFinite(n) && n > 0 ? n : 30
}

const generateRefreshTokenRaw = () => {
  return crypto.randomBytes(32).toString('base64url')
}

const signAccessToken = ({ userType, userId, sessionId }) => {
  return jwt.sign(
    { user_type: userType, session_id: sessionId },
    getJwtSecret(),
    {
      algorithm: 'HS256',
      expiresIn: getAccessTokenTtlSeconds(),
      subject: userId,
    },
  )
}

/**
 * Verify an access token. Returns { userType, userId, sessionId } on success.
 * Throws an error with code/statusCode on failure.
 */
export const verifyAccessToken = (token) => {
  try {
    const decoded = jwt.verify(token, getJwtSecret(), { algorithms: ['HS256'] })
    if (!decoded || typeof decoded !== 'object') {
      throw new Error('Malformed token')
    }
    return {
      userType: decoded.user_type,
      userId: decoded.sub,
      sessionId: decoded.session_id,
    }
  } catch (err) {
    const code = err.name === 'TokenExpiredError' ? 'TOKEN_EXPIRED' : 'TOKEN_INVALID'
    throw Object.assign(new Error(err.message || 'Invalid token'), {
      code, statusCode: 401,
    })
  }
}

/**
 * Create a new auth_sessions row and return tokens.
 * deviceInfo is optional JSONB: { user_agent, ip }
 */
export const issueTokens = async ({ userType, userId, deviceInfo }) => {
  const refreshRaw = generateRefreshTokenRaw()
  const refreshHash = await bcrypt.hash(refreshRaw, REFRESH_HASH_COST)
  const ttlDays = getRefreshTokenTtlDays()

  const [session] = await sql`
    INSERT INTO auth_sessions (user_type, user_id, refresh_token_hash, device_info, expires_at)
    VALUES (
      ${userType}, ${userId}, ${refreshHash}, ${deviceInfo ? sql.json(deviceInfo) : null},
      NOW() + (${ttlDays} || ' days')::interval
    )
    RETURNING id, expires_at
  `

  const accessToken = signAccessToken({ userType, userId, sessionId: session.id })
  const accessTtlSeconds = getAccessTokenTtlSeconds()

  return {
    access_token: accessToken,
    access_token_expires_in: accessTtlSeconds,
    refresh_token: `${session.id}.${refreshRaw}`,
    refresh_token_expires_at: session.expires_at,
    session_id: session.id,
  }
}

/**
 * Validate a refresh token and rotate it. Returns new token pair.
 * Refresh token format: "<session_id>.<raw>"
 */
export const refreshTokens = async ({ refreshToken, deviceInfo }) => {
  if (typeof refreshToken !== 'string' || !refreshToken.includes('.')) {
    throw Object.assign(new Error('Invalid refresh token'), {
      code: 'REFRESH_INVALID', statusCode: 401,
    })
  }
  const [sessionId, raw] = refreshToken.split('.', 2)

  const [row] = await sql`
    SELECT id, user_type, user_id, refresh_token_hash, expires_at, revoked_at
    FROM auth_sessions
    WHERE id = ${sessionId}
  `
  if (!row || row.revoked_at || new Date(row.expires_at) <= new Date()) {
    throw Object.assign(new Error('Refresh token expired or revoked'), {
      code: 'REFRESH_INVALID', statusCode: 401,
    })
  }
  const ok = await bcrypt.compare(raw, row.refresh_token_hash)
  if (!ok) {
    throw Object.assign(new Error('Refresh token mismatch'), {
      code: 'REFRESH_INVALID', statusCode: 401,
    })
  }

  // Rotate: issue a new raw, update the hash, bump last_used_at and expires_at
  const newRaw = generateRefreshTokenRaw()
  const newHash = await bcrypt.hash(newRaw, REFRESH_HASH_COST)
  const ttlDays = getRefreshTokenTtlDays()
  const [updated] = await sql`
    UPDATE auth_sessions
    SET refresh_token_hash = ${newHash},
        last_used_at = NOW(),
        expires_at = NOW() + (${ttlDays} || ' days')::interval,
        device_info = ${deviceInfo ? sql.json(deviceInfo) : row.device_info ?? null}
    WHERE id = ${sessionId}
    RETURNING id, user_type, user_id, expires_at
  `

  const accessToken = signAccessToken({
    userType: updated.user_type,
    userId: updated.user_id,
    sessionId: updated.id,
  })

  return {
    access_token: accessToken,
    access_token_expires_in: getAccessTokenTtlSeconds(),
    refresh_token: `${updated.id}.${newRaw}`,
    refresh_token_expires_at: updated.expires_at,
    session_id: updated.id,
    user_type: updated.user_type,
    user_id: updated.user_id,
  }
}

/**
 * Revoke a single session (logout).
 */
export const revokeSession = async (sessionId) => {
  await sql`
    UPDATE auth_sessions SET revoked_at = NOW()
    WHERE id = ${sessionId} AND revoked_at IS NULL
  `
  // Future: SADD session_id into Valkey 'revoked_sessions' with TTL = remaining access token life
}

/**
 * Revoke every session for a user (admin ban / force logout everywhere).
 */
export const revokeAllSessionsForUser = async ({ userType, userId }) => {
  await sql`
    UPDATE auth_sessions SET revoked_at = NOW()
    WHERE user_type = ${userType} AND user_id = ${userId} AND revoked_at IS NULL
  `
}
