// services/auth.service.js
const bcrypt = require("bcryptjs")
const jwt = require("jsonwebtoken")
const crypto = require("crypto")

const authDb = require("../db/auth.db")
const refreshTokenDb = require("../db/refreshToken.db")
const { queueAuditEvent } = require("./redis/dbSync.service")
const { AUDIT_ACTIONS } = require("./auditActions")
const { buildAuditMeta } = require("./audit.service")

const REUSE_GRACE_MS = Number(process.env.REFRESH_REUSE_GRACE_MS || 10000)

function httpError(statusCode, message) {
  const err = new Error(message)
  err.statusCode = statusCode
  return err
}

// Access token: kisa ömür
function signAccessToken(user) {
  const jti = crypto.randomUUID()
  const payload = {
    sub: String(user.id),
    role: user.role, // USER|MOD|ADMIN
    jti,
  }

  const expiresIn = process.env.ACCESS_TOKEN_EXPIRES_IN || "15m"
  const token = jwt.sign(payload, process.env.JWT_ACCESS_SECRET, { expiresIn })
  return { token, jti }
}

// Refresh token: opaque (JWT degil) + DB'de hash
function generateRefreshToken() {
  // 64 byte -> url-safe base64
  return crypto.randomBytes(64).toString("base64url")
}

function hashToken(token) {
  return crypto.createHash("sha256").update(token).digest("hex")
}

function refreshExpiresAt() {
  const days = Number(process.env.REFRESH_TOKEN_DAYS || 30)
  return new Date(Date.now() + days * 24 * 60 * 60 * 1000)
}

function mapUserPublic(user) {
  return {
    id: user.id,
    username: user.username,
    email: user.email,
    avatarUrl: user.avatarUrl ?? null,
    role: user.role,
  }
}

async function login({ email, password, meta = {} }) {
  const user = await authDb.findUserByEmail(email)
  if (!user) throw httpError(400, "Kullanici bulunamadi.")
  if (user.disabledAt) throw httpError(403, "Hesap devre disi.")

  const isMatch = await bcrypt.compare(password, user.passwordHash)
  if (!isMatch) throw httpError(401, "Sifre hatali.")

  const { token: accessToken } = signAccessToken(user)

  const refreshToken = generateRefreshToken()
  const tokenHash = hashToken(refreshToken)
  const familyId = crypto.randomUUID()
  const jti = crypto.randomUUID()

  await refreshTokenDb.createRefreshToken(user.id, {
    tokenHash,
    familyId,
    jti,
    expiresAt: refreshExpiresAt(),
    createdByIp: meta.ip ?? null,
    userAgent: meta.userAgent ?? null,
  })

  queueAuditEvent({
    userId: user.id,
    action: AUDIT_ACTIONS.AUTH.LOGIN_SUCCESS,
    ip: meta.ip ?? null,
    userAgent: meta.userAgent ?? null,
    meta: buildAuditMeta({ entityType: "USER", entityId: user.id }),
    createdAt: new Date().toISOString(),
  }).catch((err) => console.error("Audit queue login failed:", err?.message || err))

  return {
    accessToken,
    refreshToken,
    user: mapUserPublic(user),
  }
}

async function register({ username, email, password, meta = {} }) {
  const existingUser = await authDb.findUserByEmail(email)
  if (existingUser) throw httpError(400, "Bu e-posta zaten kayitli.")

  const passwordHash = await bcrypt.hash(password, 10)
  const user = await authDb.createUser({ username, email, passwordHash })

  const { token: accessToken } = signAccessToken(user)

  const refreshToken = generateRefreshToken()
  const tokenHash = hashToken(refreshToken)
  const familyId = crypto.randomUUID()
  const jti = crypto.randomUUID()

  await refreshTokenDb.createRefreshToken(user.id, {
    tokenHash,
    familyId,
    jti,
    expiresAt: refreshExpiresAt(),
    createdByIp: meta.ip ?? null,
    userAgent: meta.userAgent ?? null,
  })

  queueAuditEvent({
    userId: user.id,
    action: AUDIT_ACTIONS.AUTH.REGISTER,
    ip: meta.ip ?? null,
    userAgent: meta.userAgent ?? null,
    meta: buildAuditMeta({ entityType: "USER", entityId: user.id }),
    createdAt: new Date().toISOString(),
  }).catch((err) => console.error("Audit queue register failed:", err?.message || err))

  return {
    accessToken,
    refreshToken,
    user: mapUserPublic(user),
  }
}

// Refresh: rotate + reuse tespiti
async function refresh({ refreshToken, meta = {} }) {
  if (!refreshToken) throw httpError(401, "Refresh token yok")

  const tokenHash = hashToken(refreshToken)
  const existing = await refreshTokenDb.findRefreshTokenByHash(tokenHash, {
    include: { user: true },
  })

  if (!existing) throw httpError(401, "Refresh token geçersiz")

  // süresi geçmis
  if (existing.expiresAt && existing.expiresAt.getTime() < Date.now()) {
    await refreshTokenDb.revokeRefreshTokenById(existing.id)
    throw httpError(401, "Refresh token süresi dolmus")
  }

  // reuse tespiti: revoke edilmis token tekrar gelirse -> tüm aileyi kapat
  if (existing.revokedAt) {
    const revokedAt = existing.revokedAt instanceof Date ? existing.revokedAt : new Date(existing.revokedAt)
    const withinGrace =
      existing.replacedById &&
      revokedAt &&
      Date.now() - revokedAt.getTime() <= REUSE_GRACE_MS

    if (!withinGrace) {
      await refreshTokenDb.revokeRefreshTokenFamily(existing.familyId)
      throw httpError(401, "Refresh token reuse tespit edildi")
    }
  }

  const user = existing.user
  if (user?.disabledAt) throw httpError(403, "Hesap devre disi.")
  const { token: accessToken } = signAccessToken(user)

  const newRefreshToken = generateRefreshToken()
  const newTokenHash = hashToken(newRefreshToken)
  const newJti = crypto.randomUUID()

  await refreshTokenDb.rotateRefreshToken({
    oldId: existing.id,
    newToken: {
      userId: user.id,
      tokenHash: newTokenHash,
      familyId: existing.familyId, // ayni aile
      jti: newJti,
      expiresAt: refreshExpiresAt(),
    },
    meta: { ip: meta.ip ?? null, userAgent: meta.userAgent ?? null },
  })

  queueAuditEvent({
    userId: user.id,
    action: AUDIT_ACTIONS.AUTH.REFRESH,
    ip: meta.ip ?? null,
    userAgent: meta.userAgent ?? null,
    meta: buildAuditMeta({ entityType: "USER", entityId: user.id }),
    createdAt: new Date().toISOString(),
  }).catch((err) => console.error("Audit queue refresh failed:", err?.message || err))

  return {
    accessToken,
    refreshToken: newRefreshToken,
    user: mapUserPublic(user),
  }
}

async function logout({ refreshToken, meta = {} }) {
  if (!refreshToken) return
  const tokenHash = hashToken(refreshToken)

  // token yoksa sessiz geçmek genelde daha iyi (idempotent logout)
  try {
    const existing = await refreshTokenDb.findRefreshTokenByHash(tokenHash, {
      select: { userId: true },
    })
    await refreshTokenDb.revokeRefreshTokenByHash(tokenHash)
    if (existing?.userId) {
      queueAuditEvent({
        userId: existing.userId,
        action: AUDIT_ACTIONS.AUTH.LOGOUT,
        ip: meta.ip ?? null,
        userAgent: meta.userAgent ?? null,
        meta: buildAuditMeta({ entityType: "USER", entityId: existing.userId }),
        createdAt: new Date().toISOString(),
      }).catch((err) => console.error("Audit queue logout failed:", err?.message || err))
    }
  } catch (_) {}
}

async function getMe(userId) {
  const user = await authDb.findUserById(Number(userId), {
    select: { id: true, username: true, email: true, avatarUrl: true, role: true },
  })
  if (!user) throw httpError(404, "Kullanici bulunamadi")
  return user
}

module.exports = {
  login,
  register,
  refresh,
  logout,
  getMe,
}
