import bcrypt from 'bcryptjs'
import crypto from 'crypto'
import { authenticator } from 'otplib'
import { signActorToken } from '../../security/tokens'
import qrcode from 'qrcode'
import { getCarplaceHomepageContent, saveCarplaceHomepageContent } from '../../services/platformContentService'
import { sendTransactionalEmail } from '../../services/notificationService'
import * as presenter from './admin.presenter'
import * as repo from './admin.repo'
import * as billingService from './admin.billing.service'

const ADMIN_RESET_TTL_MINUTES = 60
const ADMIN_RECOVERY_CODE_COUNT = 10


function generateRecoveryCode() {
  const raw = crypto.randomBytes(9).toString('base64url').replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 12)
  return `${raw.slice(0, 4)}-${raw.slice(4, 8)}-${raw.slice(8, 12)}`
}

async function issueAdminRecoveryCodes(adminId: string) {
  const codes = Array.from({ length: ADMIN_RECOVERY_CODE_COUNT }, generateRecoveryCode)
  const hashes = await Promise.all(codes.map((code) => bcrypt.hash(code, 12)))
  await repo.replaceAdminRecoveryCodes(adminId, hashes)
  await repo.createAuditLog({
    adminUserId: adminId,
    action: 'ADMIN_2FA_RECOVERY_CODES_ISSUED',
    resource: 'AdminUser',
    resourceId: adminId,
  })
  return codes
}

async function consumeAdminRecoveryCode(adminId: string, code: string) {
  const normalized = code.trim().toUpperCase()
  if (!normalized) return false

  const codes = await repo.listUnusedAdminRecoveryCodes(adminId)
  for (const candidate of codes) {
    if (await bcrypt.compare(normalized, candidate.codeHash)) {
      await repo.markAdminRecoveryCodeUsed(candidate.id)
      await repo.createAuditLog({
        adminUserId: adminId,
        action: 'ADMIN_2FA_RECOVERY_CODE_USED',
        resource: 'AdminUser',
        resourceId: adminId,
      })
      return true
    }
  }

  return false
}

function signAdminToken(adminId: string, last2faAt?: number) {
  return signActorToken(adminId, 'admin', { expiresIn: '8h', last2faAt })
}

function toAuditJson<T>(value: T) {
  return JSON.parse(JSON.stringify(value))
}

function ensureAdminBasePath(baseUrl: string) {
  try {
    const url = new URL(baseUrl)
    const pathname = url.pathname.replace(/\/$/, '')
    if (!pathname.endsWith('/admin')) url.pathname = `${pathname}/admin`
    return url.toString().replace(/\/$/, '')
  } catch {
    const trimmed = baseUrl.replace(/\/$/, '')
    return trimmed.endsWith('/admin') ? trimmed : `${trimmed}/admin`
  }
}

export async function login(email: string, password: string, totpCode?: string, recoveryCode?: string) {
  const admin = await repo.findAdminByEmail(email)
  if (!admin || !admin.isActive) return null

  const valid = await bcrypt.compare(password, admin.passwordHash)
  if (!valid) return null

  if (admin.totpEnabled) {
    if (!totpCode && !recoveryCode) return { totpRequired: true } as const

    const validTotp = totpCode
      ? authenticator.verify({ token: totpCode, secret: admin.totpSecret! })
      : false
    const validRecoveryCode = !validTotp && recoveryCode
      ? await consumeAdminRecoveryCode(admin.id, recoveryCode)
      : false

    if (!validTotp && !validRecoveryCode) {
      return { invalidTotp: true } as const
    }
  }

  await repo.updateAdminLastLogin(admin.id)
  await repo.createAuditLog({
    adminUserId: admin.id,
    action: 'ADMIN_LOGIN',
    resource: 'AdminUser',
    resourceId: admin.id,
  })

  return presenter.presentAdminSession(admin, signAdminToken(admin.id, admin.totpEnabled ? Date.now() : undefined))
}

export async function setupTotp(adminId: string, email: string) {
  const secret = authenticator.generateSecret()
  await repo.updateAdminTotpSecret(adminId, secret)
  const otpauth = authenticator.keyuri(email, 'RentalDriveGo Admin', secret)
  const qrCode = await qrcode.toDataURL(otpauth)
  return { secret, qrCode }
}

export async function verifyTotp(adminId: string, code: string) {
  const admin = await repo.findAdminByIdOrThrow(adminId)
  if (!admin.totpSecret) return false

  const valid = authenticator.verify({ token: code, secret: admin.totpSecret })
  if (!valid) return false

  await repo.enableAdminTotp(adminId)
  await repo.createAuditLog({
    adminUserId: adminId,
    action: 'ADMIN_2FA_VERIFIED',
    resource: 'AdminUser',
    resourceId: adminId,
  })
  const recoveryCodes = await issueAdminRecoveryCodes(adminId)
  return {
    ...presenter.presentAdminSession({ ...admin, totpEnabled: true }, signAdminToken(adminId, Date.now())),
    recoveryCodes,
  }
}

export async function regenerateRecoveryCodes(adminId: string) {
  return { recoveryCodes: await issueAdminRecoveryCodes(adminId) }
}

export async function forgotPassword(email: string) {
  const admin = await repo.findAdminByEmail(email)
  if (!admin || !admin.isActive) return

  const rawToken = crypto.randomBytes(32).toString('hex')
  const expiresAt = new Date(Date.now() + ADMIN_RESET_TTL_MINUTES * 60 * 1000)
  await repo.setAdminPasswordReset(admin.id, rawToken, expiresAt)

  const adminUrl = ensureAdminBasePath(
    process.env.ADMIN_URL ?? process.env.NEXT_PUBLIC_ADMIN_URL ?? 'http://localhost:3000/admin',
  )
  const resetUrl = `${adminUrl}/reset-password?token=${rawToken}`

  await sendTransactionalEmail({
    to: admin.email,
    subject: 'Reset your RentalDriveGo admin password',
    html: `<p>Hi ${admin.firstName},</p><p><a href="${resetUrl}">Reset password</a></p><p>Expires in ${ADMIN_RESET_TTL_MINUTES} minutes.</p>`,
    text: `Hi ${admin.firstName},\n\nReset here: ${resetUrl}\n\nExpires in ${ADMIN_RESET_TTL_MINUTES} minutes.`,
  }).catch((err) => console.error('[AdminForgotPassword]', err?.message))
}

export async function resetPassword(token: string, password: string) {
  const admin = await repo.findAdminByResetToken(token)
  if (!admin) return false

  await repo.updateAdminPassword(admin.id, await bcrypt.hash(password, 12))
  return true
}

export async function listCompanies(query: { q?: string; status?: string; plan?: string; page: number; pageSize: number }) {
  const { data, total } = await repo.listCompaniesPage(query)
  return presenter.presentPaginated(data, total, query.page, query.pageSize)
}

export function getCompany(id: string) {
  return repo.getCompanyDetail(id)
}

export async function updateCompany(id: string, body: any, adminId: string, ip?: string) {
  const before = await repo.getCompanyUpdateSnapshot(id)
  const updated = await repo.applyCompanyUpdate(id, body, before)
  await repo.createAuditLog({
    adminUserId: adminId,
    action: 'UPDATE_COMPANY',
    resource: 'Company',
    resourceId: id,
    companyId: id,
    before: toAuditJson(before),
    after: toAuditJson(body),
    ipAddress: ip,
  })
  return updated
}

export async function setCompanyStatus(id: string, status: string, reason: string | undefined, adminId: string, ip?: string) {
  const before = await repo.getCompanyUpdateSnapshot(id)
  const updated = await repo.updateCompanyStatus(id, status)
  await repo.createAuditLog({
    adminUserId: adminId,
    action: `SET_COMPANY_STATUS_${status}`,
    resource: 'Company',
    resourceId: id,
    companyId: id,
    before: { status: before.status },
    after: { status },
    note: reason,
    ipAddress: ip,
  })
  return updated
}

export async function deleteCompany(id: string, adminId: string, ip?: string) {
  await repo.deleteCompany(id)
  await repo.createAuditLog({
    adminUserId: adminId,
    action: 'DELETE_COMPANY',
    resource: 'Company',
    resourceId: id,
    ipAddress: ip,
  })
}

export async function impersonateCompany(id: string, adminId: string, ip?: string, reason?: string, durationMinutes = 15) {
  const company = await repo.getCompanyForImpersonation(id)
  const ttlMinutes = Math.min(Math.max(durationMinutes, 1), 30)
  const employeeId = company.employees[0]?.id
  if (!employeeId) throw new Error('Company has no employee account to impersonate')
  const token = signActorToken(employeeId, 'employee', { expiresIn: `${ttlMinutes}m` as any })

  await repo.createAuditLog({
    adminUserId: adminId,
    action: 'IMPERSONATE_COMPANY',
    resource: 'Company',
    resourceId: id,
    companyId: id,
    note: reason,
    before: { originalAdminId: adminId },
    after: { targetCompanyId: id, durationMinutes: ttlMinutes },
    ipAddress: ip,
  })

  return { token, expiresIn: ttlMinutes * 60, impersonation: { companyId: id, reason, durationMinutes: ttlMinutes } }
}

export async function listRenters(query: { q?: string; blocked?: string; page: number; pageSize: number }) {
  const { data, total } = await repo.listRentersPage(query)
  return presenter.presentPaginated(data, total, query.page, query.pageSize)
}

export function setRenterActive(id: string, isActive: boolean) {
  return repo.updateRenterActive(id, isActive)
}

export function getPlatformMetrics() {
  return repo.getPlatformMetricCounts()
}

export async function listNotifications(query: { channel?: string; status?: string; companyId?: string; page: number; pageSize: number }) {
  const { data, total } = await repo.listNotificationsPage(query)
  return presenter.presentPaginated(data, total, query.page, query.pageSize)
}

export async function getAuditLogs(query: { adminId?: string; action?: string; companyId?: string; entityId?: string; page: number; pageSize: number }) {
  const { data, total } = await repo.listAuditLogsPage(query)
  return presenter.presentPaginated(data, total, query.page, query.pageSize)
}

export async function listAdmins() {
  const admins = await repo.listAdmins()
  return admins.map((admin: any) => presenter.presentAdminUser(admin))
}

export async function createAdmin(body: { email: string; firstName: string; lastName: string; role: string; password: string; permissions?: any[] }) {
  const admin = await repo.createAdmin({
    ...body,
    passwordHash: await bcrypt.hash(body.password, 12),
  })
  return presenter.presentAdminUser(admin)
}

export async function updateAdmin(
  id: string,
  body: {
    email?: string
    firstName?: string
    lastName?: string
    role?: string
    password?: string
    isActive?: boolean
  },
) {
  const admin = await repo.updateAdmin(id, {
    ...(body.email !== undefined ? { email: body.email } : {}),
    ...(body.firstName !== undefined ? { firstName: body.firstName } : {}),
    ...(body.lastName !== undefined ? { lastName: body.lastName } : {}),
    ...(body.role !== undefined ? { role: body.role } : {}),
    ...(body.isActive !== undefined ? { isActive: body.isActive } : {}),
    ...(body.password ? { passwordHash: await bcrypt.hash(body.password, 12) } : {}),
  })

  return presenter.presentAdminUser(admin)
}

export function updateAdminRole(id: string, role: string) {
  return repo.updateAdminRole(id, role)
}

export async function updateAdminPermissions(id: string, permissions: any[]) {
  const admin = await repo.replaceAdminPermissions(id, permissions)
  return presenter.presentAdminUser(admin)
}

export async function getBilling(query: { q?: string; status?: string; plan?: string; page: number; pageSize: number }) {
  const { data, total, stats } = await billingService.listBillingAccounts(query)
  return presenter.presentPaginated(data, total, query.page, query.pageSize, { stats })
}

export async function getCompanyInvoices(companyId: string, query: { page: number; pageSize: number }) {
  const detail = await billingService.getBillingAccountDetail(companyId)
  const start = (query.page - 1) * query.pageSize
  const end = start + query.pageSize
  const data = detail.invoices.slice(start, end)
  const total = detail.invoices.length
  return presenter.presentPaginated(data, total, query.page, query.pageSize)
}

export async function getInvoicePdf(invoiceId: string) {
  return billingService.getInvoicePdf(invoiceId)
}

export function getBillingAccountDetail(companyId: string) {
  return billingService.getBillingAccountDetail(companyId)
}

export function updateBillingAccount(
  billingAccountId: string,
  data: Parameters<typeof billingService.updateBillingAccount>[1],
  adminId: string,
  ip?: string,
) {
  return billingService.updateBillingAccount(billingAccountId, data, adminId, ip)
}

export function setDunningPaused(billingAccountId: string, paused: boolean, adminId: string, ip?: string) {
  return billingService.setDunningPaused(billingAccountId, paused, adminId, ip)
}

export function createBillingInvoice(
  billingAccountId: string,
  data: Parameters<typeof billingService.createDraftInvoice>[1],
  adminId: string,
  ip?: string,
) {
  return billingService.createDraftInvoice(billingAccountId, data, adminId, ip)
}

export function finalizeBillingInvoice(invoiceId: string, adminId: string, ip?: string) {
  return billingService.finalizeInvoice(invoiceId, adminId, ip)
}

export function payBillingInvoice(
  invoiceId: string,
  data: Parameters<typeof billingService.payInvoice>[1],
  adminId: string,
  ip?: string,
) {
  return billingService.payInvoice(invoiceId, data, adminId, ip)
}

export function retryBillingInvoicePayment(
  invoiceId: string,
  data: Parameters<typeof billingService.retryInvoicePayment>[1],
  adminId: string,
  ip?: string,
) {
  return billingService.retryInvoicePayment(invoiceId, data, adminId, ip)
}

export function voidBillingInvoice(invoiceId: string, reason: string, adminId: string, ip?: string) {
  return billingService.voidInvoice(invoiceId, reason, adminId, ip)
}

export function markBillingInvoiceUncollectible(invoiceId: string, reason: string, adminId: string, ip?: string) {
  return billingService.markInvoiceUncollectible(invoiceId, reason, adminId, ip)
}

export function issueBillingCreditNote(
  invoiceId: string,
  data: Parameters<typeof billingService.issueCreditNote>[1],
  adminId: string,
  ip?: string,
) {
  return billingService.issueCreditNote(invoiceId, data, adminId, ip)
}

export function issueBillingRefund(
  invoiceId: string,
  data: Parameters<typeof billingService.issueRefund>[1],
  adminId: string,
  ip?: string,
) {
  return billingService.issueRefund(invoiceId, data, adminId, ip)
}

export function getCarplaceHomepage() {
  return getCarplaceHomepageContent()
}

export async function updateCarplaceHomepage(homepage: any, adminId: string, ip?: string) {
  const saved = await saveCarplaceHomepageContent(homepage)
  await repo.createAuditLog({
    adminUserId: adminId,
    action: 'UPDATE',
    resource: 'CarplaceHomepage',
    after: toAuditJson(saved),
    ipAddress: ip,
    userAgent: undefined,
  })
  return saved
}

export function getPricingConfigs() {
  return repo.listPricingConfigs()
}

export async function updatePricingConfigs(entries: { plan: string; billingPeriod: string; amount: number }[], adminId: string, ip?: string) {
  const results = await Promise.all(
    entries.map((e) => repo.upsertPricingConfig(e.plan, e.billingPeriod, e.amount, adminId)),
  )
  await repo.createAuditLog({
    adminUserId: adminId,
    action: 'UPDATE',
    resource: 'PricingConfig',
    after: toAuditJson(results),
    ipAddress: ip,
    userAgent: undefined,
  })
  return results
}

export function listPlanFeatures() {
  return repo.listPlanFeatures()
}

export async function createPlanFeature(data: Parameters<typeof repo.createPlanFeature>[0], adminId: string, ip?: string) {
  const feature = await repo.createPlanFeature(data)
  await repo.createAuditLog({
    adminUserId: adminId,
    action: 'CREATE',
    resource: 'PlanFeature',
    after: toAuditJson(feature),
    ipAddress: ip,
    userAgent: undefined,
  })
  return feature
}

export async function updatePlanFeature(id: string, data: Parameters<typeof repo.updatePlanFeature>[1], adminId: string, ip?: string) {
  const feature = await repo.updatePlanFeature(id, data)
  await repo.createAuditLog({
    adminUserId: adminId,
    action: 'UPDATE',
    resource: 'PlanFeature',
    resourceId: id,
    after: toAuditJson(feature),
    ipAddress: ip,
    userAgent: undefined,
  })
  return feature
}

export async function deletePlanFeature(id: string, adminId: string, ip?: string) {
  await repo.deletePlanFeature(id)
  await repo.createAuditLog({
    adminUserId: adminId,
    action: 'DELETE',
    resource: 'PlanFeature',
    resourceId: id,
    ipAddress: ip,
    userAgent: undefined,
  })
}

// ─── Promotions ────────────────────────────────────────────────────────────

export function listPromotions() {
  return repo.listPromotions()
}

export async function createPromotion(data: Parameters<typeof repo.createPromotion>[0], adminId: string, ip?: string) {
  const promo = await repo.createPromotion({ ...data, createdBy: adminId })
  await repo.createAuditLog({
    adminUserId: adminId, action: 'CREATE', resource: 'PricingPromotion',
    after: toAuditJson(promo), ipAddress: ip, userAgent: undefined,
  })
  return promo
}

export async function updatePromotion(id: string, data: Parameters<typeof repo.updatePromotion>[1], adminId: string, ip?: string) {
  const promo = await repo.updatePromotion(id, data)
  await repo.createAuditLog({
    adminUserId: adminId, action: 'UPDATE', resource: 'PricingPromotion',
    after: toAuditJson(promo), ipAddress: ip, userAgent: undefined,
  })
  return promo
}

export async function deletePromotion(id: string, adminId: string, ip?: string) {
  await repo.deletePromotion(id)
  await repo.createAuditLog({
    adminUserId: adminId, action: 'DELETE', resource: 'PricingPromotion',
    entityId: id, ipAddress: ip, userAgent: undefined,
  })
}
