import { User } from '../models/User.js'
import { conflict, unauthorized } from '../lib/errors.js'
import {
  hashPassword,
  hashToken,
  newId,
  signAccessToken,
  signRefreshToken,
  verifyPassword,
  type Identity,
} from '../lib/tokens.js'

const publicUser = (user: { _id: { toString(): string }; name: string; email: string; avatar?: string | null }) => ({
  id: user._id.toString(),
  name: user.name,
  email: user.email,
  avatar: user.avatar ?? null,
})

const issueSession = async (identity: Identity, userId: string) => {
  const accessToken = signAccessToken(identity)
  const refreshToken = signRefreshToken(identity)
  await User.updateOne({ _id: userId }, { refreshTokenHash: hashToken(refreshToken) })
  return { accessToken, refreshToken }
}

export const signup = async (input: { name: string; email: string; password: string }) => {
  const existing = await User.findOne({ email: input.email.toLowerCase() }).lean()
  if (existing) throw conflict('An account with that email already exists')
  const user = await User.create({
    name: input.name,
    email: input.email.toLowerCase(),
    passwordHash: await hashPassword(input.password),
  })
  const identity: Identity = { sub: user._id.toString(), kind: 'user', name: user.name, email: user.email }
  const tokens = await issueSession(identity, user._id.toString())
  return { user: publicUser(user), ...tokens }
}

export const login = async (input: { email: string; password: string }) => {
  const user = await User.findOne({ email: input.email.toLowerCase() })
  if (!user?.passwordHash) throw unauthorized('Invalid email or password')
  const matches = await verifyPassword(input.password, user.passwordHash)
  if (!matches) throw unauthorized('Invalid email or password')
  const identity: Identity = { sub: user._id.toString(), kind: 'user', name: user.name, email: user.email }
  const tokens = await issueSession(identity, user._id.toString())
  return { user: publicUser(user), ...tokens }
}

export const refreshSession = async (userId: string, refreshToken: string) => {
  const user = await User.findById(userId)
  if (!user || user.refreshTokenHash !== hashToken(refreshToken)) {
    throw unauthorized('Invalid refresh token')
  }
  const identity: Identity = { sub: user._id.toString(), kind: 'user', name: user.name, email: user.email }
  const tokens = await issueSession(identity, user._id.toString())
  return { user: publicUser(user), ...tokens }
}

export const logout = async (userId: string) => {
  await User.updateOne({ _id: userId }, { $unset: { refreshTokenHash: 1 } })
}

export const guestSession = (name: string) => {
  const identity: Identity = { sub: `guest_${newId()}`, kind: 'guest', name }
  return { identity, accessToken: signAccessToken(identity) }
}

export const upsertGoogleUser = async (profile: { sub: string; email: string; name: string; picture?: string }) => {
  let user = await User.findOne({ $or: [{ googleId: profile.sub }, { email: profile.email.toLowerCase() }] })
  if (!user) {
    user = await User.create({
      name: profile.name,
      email: profile.email.toLowerCase(),
      googleId: profile.sub,
      avatar: profile.picture,
    })
  } else if (!user.googleId) {
    user.googleId = profile.sub
    if (profile.picture) user.avatar = profile.picture
    await user.save()
  }
  const identity: Identity = { sub: user._id.toString(), kind: 'user', name: user.name, email: user.email }
  const tokens = await issueSession(identity, user._id.toString())
  return { user: publicUser(user), ...tokens }
}

export const currentUser = async (identity: Identity) => {
  if (identity.kind === 'guest') {
    return { id: identity.sub, name: identity.name, email: null, avatar: null, kind: 'guest' as const }
  }
  const user = await User.findById(identity.sub).lean()
  if (!user) throw unauthorized()
  return { ...publicUser(user), kind: 'user' as const }
}
