'use strict'
const cfg = require('./config')
const crypto = require('crypto')
const { log, json, parseBody, httpRequest, erpFetch } = require('./helpers')
const { sendEmail } = require('./email')

// Strong-but-readable password: 4 base32 chunks separated by dashes,
// e.g. "X7K2-9NQB-4GHM-3RTW". Avoids look-alike chars (0/O, 1/I/L) so
// the user can copy it from a Slack/SMS without guessing case.
function generateInvitePassword () {
  const alphabet = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' // no 0/O/1/I/L
  const out = []
  const buf = crypto.randomBytes(16)
  for (let i = 0; i < 16; i++) out.push(alphabet[buf[i] % alphabet.length])
  return `${out.slice(0,4).join('')}-${out.slice(4,8).join('')}-${out.slice(8,12).join('')}-${out.slice(12,16).join('')}`
}
let mysql = null
try { mysql = require('mysql2/promise') } catch (e) { /* optional */ }

const CAP_DEFS = {
  Dashboard:      { view_dashboard_kpi: 'Voir KPIs (revenus, AR)' },
  Clients:        { view_clients: 'Voir la liste clients', view_client_detail: 'Voir le détail client', edit_customer: 'Modifier les champs client' },
  Finance:        { view_invoices: 'Voir les factures', create_invoices: 'Créer/modifier factures', view_payments: 'Voir les paiements', create_payments: 'Créer paiements', edit_subscription_price: 'Modifier prix abonnement' },
  Dispatch:       { view_all_jobs: 'Voir tous les jobs', view_own_jobs: 'Voir ses propres jobs', assign_jobs: 'Assigner/réassigner', create_jobs: 'Créer des jobs' },
  'Équipement':   { view_equipment: 'Voir diagnostics équipement', reboot_provision: 'Reboot/provisionner' },
  Tickets:        { view_all_tickets: 'Voir tous les tickets', view_own_tickets: 'Voir ses tickets', manage_tickets: 'Créer/fermer tickets' },
  Communication:  { send_sms: 'Envoyer SMS', make_calls: 'Passer des appels' },
  'Téléphonie':   { manage_telephony: 'Gérer Fonoster/trunks' },
  Administration: { view_settings: 'Voir paramètres', manage_settings: 'Modifier paramètres', manage_users: 'Gérer utilisateurs', delete_records: 'Supprimer des enregistrements', manage_permissions: 'Modifier les permissions' },
}
const CAPABILITIES = Object.entries(CAP_DEFS).flatMap(([cat, caps]) => Object.entries(caps).map(([key, label]) => ({ key, label, category: cat })))

const OPS_GROUPS = ['admin', 'sysadmin', 'tech', 'support', 'comptabilite', 'facturation', 'dev']

function akFetch (path, method = 'GET', body = null) {
  if (!cfg.AUTHENTIK_TOKEN) throw new Error('AUTHENTIK_TOKEN not configured')
  return httpRequest(cfg.AUTHENTIK_URL, '/api/v3' + path, {
    method, body, headers: { Authorization: 'Bearer ' + cfg.AUTHENTIK_TOKEN }, timeout: 10000,
  })
}

let groupCache = null, groupCacheTs = 0

async function fetchGroups () {
  if (groupCache && Date.now() - groupCacheTs < 60000) return groupCache
  const r = await akFetch('/core/groups/?page_size=100')
  if (r.status !== 200) throw new Error('Authentik groups fetch failed: ' + r.status)
  groupCache = r.data.results
  groupCacheTs = Date.now()
  return groupCache
}

function invalidateCache () { groupCache = null; groupCacheTs = 0 }

async function findUserByEmail (email) {
  const r = await akFetch('/core/users/?search=' + encodeURIComponent(email) + '&page_size=5')
  if (r.status !== 200) return { error: 'Authentik user lookup failed', status: 502 }
  const user = r.data.results?.find(u => u.email?.toLowerCase() === email.toLowerCase())
  return user ? { user } : { error: 'User not found: ' + email, status: 404 }
}

function validatePermissions (body) {
  const validKeys = new Set(CAPABILITIES.map(c => c.key))
  const result = {}
  for (const [key, val] of Object.entries(body)) {
    if (validKeys.has(key) && typeof val === 'boolean') result[key] = val
  }
  return result
}

async function handle (req, res, method, path, url) {
  if (!cfg.AUTHENTIK_TOKEN) return json(res, 503, { error: 'Authentik not configured' })

  try {
    const parts = path.replace('/auth/', '').split('/').filter(Boolean)
    const resource = parts[0]

    if (resource === 'capabilities' && method === 'GET') {
      return json(res, 200, CAPABILITIES)
    }

    if (resource === 'permissions' && method === 'GET') {
      const email = url.searchParams.get('email') || req.headers['x-authentik-email']
      if (!email) return json(res, 400, { error: 'Missing email parameter' })

      const lookup = await findUserByEmail(email)
      if (lookup.error) return json(res, lookup.status, { error: lookup.error })
      const { user } = lookup

      const allGroups = await fetchGroups()
      const userGroupPks = new Set(user.groups || [])
      const userGroups = allGroups.filter(g => userGroupPks.has(g.pk) && OPS_GROUPS.includes(g.name))

      const merged = {}
      for (const cap of CAPABILITIES) merged[cap.key] = false
      for (const g of userGroups) {
        const perms = g.attributes?.ops_permissions || {}
        for (const [key, val] of Object.entries(perms)) {
          if (val === true) merged[key] = true
        }
      }

      const overrides = user.attributes?.ops_permissions_override || {}
      for (const [key, val] of Object.entries(overrides)) {
        if (typeof val === 'boolean') merged[key] = val
      }

      return json(res, 200, {
        email: user.email, username: user.username, name: user.name,
        groups: userGroups.map(g => g.name), is_superuser: user.is_superuser || false,
        capabilities: merged, overrides,
      })
    }

    if (resource === 'groups' && !parts[1] && method === 'GET') {
      const allGroups = await fetchGroups()
      const opsGroups = allGroups
        .filter(g => OPS_GROUPS.includes(g.name))
        .map(g => ({
          pk: g.pk, name: g.name, num_users: g.users_obj?.length || 0,
          is_superuser: g.is_superuser || false, permissions: g.attributes?.ops_permissions || {},
        }))
        .sort((a, b) => OPS_GROUPS.indexOf(a.name) - OPS_GROUPS.indexOf(b.name))
      return json(res, 200, { groups: opsGroups, capabilities: CAPABILITIES })
    }

    if (resource === 'groups' && parts[2] === 'permissions' && method === 'PUT') {
      const groupName = decodeURIComponent(parts[1])
      const allGroups = await fetchGroups()
      const group = allGroups.find(g => g.name === groupName)
      if (!group) return json(res, 404, { error: 'Group not found: ' + groupName })

      const perms = validatePermissions(await parseBody(req))
      const attrs = { ...(group.attributes || {}), ops_permissions: perms }
      const r = await akFetch('/core/groups/' + group.pk + '/', 'PATCH', { attributes: attrs })
      if (r.status !== 200) return json(res, 502, { error: 'Authentik update failed: ' + r.status })

      invalidateCache()
      log(`Auth: updated permissions for group ${groupName}: ${Object.keys(perms).filter(k => perms[k]).length} capabilities enabled`)
      return json(res, 200, { ok: true, group: groupName, permissions: perms })
    }

    if (resource === 'users' && !parts[1] && method === 'GET') {
      const page = url.searchParams.get('page') || '1'
      const search = url.searchParams.get('search') || ''
      const groupFilter = url.searchParams.get('group') || ''
      let akPath = `/core/users/?page=${page}&page_size=100&ordering=username`
      if (search) akPath += '&search=' + encodeURIComponent(search)
      if (groupFilter) {
        const allGrps = await fetchGroups()
        const grp = allGrps.find(g => g.name === groupFilter)
        if (grp) akPath += '&groups_by_pk=' + grp.pk
      }

      const r = await akFetch(akPath)
      if (r.status !== 200) return json(res, 502, { error: 'Authentik users fetch failed' })

      const allGroups = await fetchGroups()
      const groupMap = new Map(allGroups.map(g => [g.pk, g.name]))

      const users = r.data.results
        .filter(u => !u.username.startsWith('ak-outpost'))
        .map(u => ({
          pk: u.pk, username: u.username, email: u.email, name: u.name,
          is_active: u.is_active, is_superuser: u.is_superuser,
          groups: (u.groups || []).map(pk => groupMap.get(pk)).filter(n => OPS_GROUPS.includes(n)),
          overrides: u.attributes?.ops_permissions_override || {},
          last_login: u.last_login,
        }))

      return json(res, 200, { users, total: r.data.pagination?.count || users.length })
    }

    if (resource === 'users' && parts[2] === 'overrides' && method === 'PUT') {
      const email = decodeURIComponent(parts[1])
      const lookup = await findUserByEmail(email)
      if (lookup.error) return json(res, lookup.status, { error: lookup.error })
      const { user } = lookup

      const overrides = validatePermissions(await parseBody(req))
      const attrs = { ...(user.attributes || {}), ops_permissions_override: overrides }
      const r = await akFetch('/core/users/' + user.pk + '/', 'PATCH', { attributes: attrs })
      if (r.status !== 200) return json(res, 502, { error: 'Authentik update failed: ' + r.status })

      log(`Auth: updated overrides for ${email}: ${JSON.stringify(overrides)}`)
      return json(res, 200, { ok: true, email, overrides })
    }

    if (resource === 'users' && parts[2] === 'groups' && method === 'PUT') {
      const email = decodeURIComponent(parts[1])
      const lookup = await findUserByEmail(email)
      if (lookup.error) return json(res, lookup.status, { error: lookup.error })
      const { user } = lookup

      const { groups: requestedGroups = [] } = await parseBody(req)
      const allGroups = await fetchGroups()
      const opsGroupMap = new Map(allGroups.filter(g => OPS_GROUPS.includes(g.name)).map(g => [g.name, g.pk]))

      const currentNonOps = (user.groups || []).filter(pk => {
        const g = allGroups.find(gr => gr.pk === pk)
        return g && !OPS_GROUPS.includes(g.name)
      })
      const finalGroups = [...currentNonOps, ...requestedGroups.map(n => opsGroupMap.get(n)).filter(Boolean)]

      const r = await akFetch('/core/users/' + user.pk + '/', 'PATCH', { groups: finalGroups })
      if (r.status !== 200) return json(res, 502, { error: 'Authentik update failed: ' + r.status })

      invalidateCache()
      log(`Auth: updated groups for ${email}: [${requestedGroups.join(', ')}]`)
      return json(res, 200, { ok: true, email, groups: requestedGroups })
    }

    // POST /auth/users — create a new user in Authentik AND ERPNext.
    // Body: { email, full_name, groups? = ['sysadmin'], roles? = ['Employee'] }
    // Behaviour:
    //   1. Authentik: create the user (random password), assign requested
    //      OPS_GROUPS, then trigger a recovery-link email so the user picks
    //      their own password on first login.
    //   2. ERPNext: create the matching User record (System User by default,
    //      with Authentik as the Social Login source) so OAuth2 finds them
    //      on first SSO login. Sends the standard ERPNext welcome email.
    // Returns the merged user record consumable by the Settings UI.
    if (resource === 'users' && !parts[1] && method === 'POST') {
      const body = await parseBody(req)
      const email = (body.email || '').trim().toLowerCase()
      const fullName = (body.full_name || '').trim()
      const requestedGroups = Array.isArray(body.groups) ? body.groups.filter(g => OPS_GROUPS.includes(g)) : ['sysadmin']
      const roles = Array.isArray(body.roles) && body.roles.length ? body.roles : ['Employee']
      if (!email || !fullName) return json(res, 400, { error: 'email + full_name required' })
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return json(res, 400, { error: 'invalid email' })

      // 1a. Pre-flight: did this user already exist on either side? Don't
      //     fail outright — the dispatcher might be re-inviting somebody
      //     after a botched first try; re-send the recovery + re-create
      //     ERPNext record if missing.
      const existing = await akFetch('/core/users/?search=' + encodeURIComponent(email) + '&page_size=5')
      let akUser = existing.data?.results?.find(u => u.email?.toLowerCase() === email)

      // 1b. Username derivation — Authentik usernames are commonly the
      //     local part of the email; collisions get a numeric suffix.
      const baseUser = email.split('@')[0].replace(/[^a-z0-9._-]/gi, '').toLowerCase() || 'user'
      let username = baseUser
      if (!akUser) {
        let suffix = 0
        // eslint-disable-next-line no-constant-condition
        while (true) {
          const probe = await akFetch('/core/users/?username=' + encodeURIComponent(username))
          if (!probe.data?.results?.length) break
          suffix++; username = baseUser + suffix
          if (suffix > 50) return json(res, 500, { error: 'Could not allocate Authentik username' })
        }
      }

      // 1c. Resolve requested group names → Authentik group PKs.
      const allGroups = await fetchGroups()
      const groupPks = requestedGroups
        .map(name => allGroups.find(g => g.name === name)?.pk)
        .filter(Boolean)

      // 1d. Create OR patch the Authentik user. Always end with the requested
      //     groups attached; resetting if necessary keeps the call idempotent.
      if (!akUser) {
        const create = await akFetch('/core/users/', 'POST', {
          username, name: fullName, email,
          is_active: true, groups: groupPks, attributes: {},
        })
        if (create.status >= 400) {
          log('Authentik user create failed:', create.status, JSON.stringify(create.data))
          return json(res, 502, { error: 'Authentik create failed', detail: create.data })
        }
        akUser = create.data
      } else {
        const patch = await akFetch('/core/users/' + akUser.pk + '/', 'PATCH', {
          name: fullName, is_active: true, groups: groupPks,
        })
        if (patch.status !== 200) {
          log('Authentik user patch failed:', patch.status, JSON.stringify(patch.data))
        } else {
          akUser = patch.data
        }
      }

      // 1e. Set a temporary password + email it via the hub's own SMTP.
      //     We tried Authentik's recovery_email/ first but the brand has
      //     no flow_recovery configured ("No recovery flow set"), and
      //     Authentik's global SMTP is also unset. Rather than build out
      //     a full recovery flow via API right now, we generate a
      //     readable temp password on our side, set it via /set_password/
      //     and mail it to the user via Mailjet (already wired into the
      //     hub for ERPNext invoice emails). The user can change it on
      //     first login. Password is also returned to the admin so they
      //     can hand it over via Slack/SMS if the email gets stuck.
      const tempPassword = generateInvitePassword()
      let passwordSet = false
      try {
        const sp = await akFetch('/core/users/' + akUser.pk + '/set_password/', 'POST', { password: tempPassword })
        passwordSet = sp.status >= 200 && sp.status < 300
        if (!passwordSet) log('set_password failed:', sp.status, JSON.stringify(sp.data).slice(0, 200))
      } catch (e) { log('set_password threw:', e.message) }

      let emailSent = false
      if (passwordSet) {
        try {
          await sendEmail({
            to: email,
            subject: 'Bienvenue chez Gigafibre — votre accès',
            html: `<p>Bonjour ${fullName},</p>
              <p>Un compte vient d'être créé pour vous. Pour vous connecter, utilisez :</p>
              <ul>
                <li><b>URL ERPNext</b> : <a href="https://erp.gigafibre.ca/">https://erp.gigafibre.ca/</a></li>
                <li><b>Identifiant</b> : ${email}</li>
                <li><b>Mot de passe temporaire</b> : <code style="background:#f3f4f6;padding:4px 8px;border-radius:4px;font-size:1.05em">${tempPassword}</code></li>
              </ul>
              <p>À votre première connexion, changez votre mot de passe via votre profil.</p>
              <p>Vous pouvez aussi passer par <a href="https://auth.targo.ca/">auth.targo.ca</a> pour le SSO.</p>
              <hr/>
              <p style="font-size:0.85em;color:#6b7280">Cet email a été envoyé automatiquement par l'application interne Gigafibre.</p>`,
          })
          emailSent = true
        } catch (e) { log('invite email failed:', e.message) }
      }

      // 2. Create the matching ERPNext User. Frappe wants `roles` as a child
      //    table of {role: <Role Name>} rows; the Authentik OAuth2 login
      //    matches by `email` so this record is what gets logged in to.
      let erpResult = null
      try {
        // Skip if already exists.
        const probe = await erpFetch('/api/resource/User/' + encodeURIComponent(email))
        if (probe.status === 200) {
          erpResult = { ok: true, existing: true, name: email }
        } else {
          const erpBody = {
            email, first_name: fullName.split(' ')[0],
            last_name: fullName.split(' ').slice(1).join(' '),
            full_name: fullName,
            user_type: 'System User', enabled: 1,
            send_welcome_email: 1,
            roles: roles.map(r => ({ role: r })),
            social_logins: [{ provider: 'authentik', userid: email }],
          }
          const create = await erpFetch('/api/resource/User', { method: 'POST', body: JSON.stringify(erpBody) })
          if (create.status >= 400) {
            log('ERPNext User create failed:', create.status, JSON.stringify(create.data).slice(0, 400))
            erpResult = { ok: false, error: 'ERPNext create failed: ' + create.status }
          } else {
            erpResult = { ok: true, existing: false, name: create.data?.data?.name || email }
          }
        }
      } catch (e) { log('ERPNext User flow failed:', e.message); erpResult = { ok: false, error: e.message } }

      log(`Auth: invited ${email} (groups=[${requestedGroups.join(',')}], erp=${erpResult?.ok ? 'ok' : 'failed'}, email=${emailSent ? 'sent' : 'failed'})`)
      return json(res, 200, {
        ok: true,
        user: {
          pk: akUser.pk, username: akUser.username, email: akUser.email, name: akUser.name,
          is_active: akUser.is_active, groups: requestedGroups, overrides: {},
        },
        erpnext: erpResult,
        // The temp password is the canonical credential for this invite.
        // Returned even when emailSent=true so the admin can verify or
        // hand it over manually if the user can't find the email.
        temp_password: passwordSet ? tempPassword : null,
        password_set: passwordSet,
        email_sent: emailSent,
      })
    }

    if (resource === 'sync-legacy' && method === 'POST') {
      if (!mysql) return json(res, 503, { error: 'mysql2 not installed' })

      const body = await parseBody(req)
      const dryRun = body.dry_run !== false // default true = preview only

      let conn
      try {
        conn = await mysql.createConnection({
          host: cfg.LEGACY_DB_HOST, user: cfg.LEGACY_DB_USER,
          password: cfg.LEGACY_DB_PASS, database: cfg.LEGACY_DB_NAME,
        })
        const [rows] = await conn.execute(
          'SELECT email, group_ad, first_name, last_name, status FROM staff WHERE status = 1 AND email != "" AND group_ad != ""'
        )
        await conn.end()

        const allGroups = await fetchGroups()
        const opsGroupMap = new Map()
        for (const g of allGroups) {
          if (OPS_GROUPS.includes(g.name)) opsGroupMap.set(g.name, g)
        }

        let akUsers = [], page = 1
        while (true) {
          const r = await akFetch('/core/users/?page=' + page + '&page_size=100&ordering=username')
          if (r.status !== 200) break
          akUsers = akUsers.concat(r.data.results || [])
          if (!r.data.pagination || page >= r.data.pagination.total_pages) break
          page++
        }

        const emailMap = new Map()
        for (const u of akUsers) {
          if (u.email) emailMap.set(u.email.toLowerCase(), u)
        }

        const report = { matched: [], not_found: [], already_ok: [], errors: [] }

        for (const row of rows) {
          const legacyEmail = row.email.toLowerCase().trim()
          const targetGroup = row.group_ad.trim()
          if (!opsGroupMap.has(targetGroup)) continue

          const akUser = emailMap.get(legacyEmail)
          if (!akUser) {
            report.not_found.push({ email: legacyEmail, legacy_group: targetGroup, name: `${row.first_name} ${row.last_name}` })
            continue
          }

          const group = opsGroupMap.get(targetGroup)
          const userGroupPks = new Set(akUser.groups || [])

          if (userGroupPks.has(group.pk)) {
            report.already_ok.push({ email: legacyEmail, group: targetGroup })
            continue
          }

          const entry = { email: legacyEmail, username: akUser.username, group: targetGroup, user_pk: akUser.pk }

          if (!dryRun) {
            const newGroups = [...(akUser.groups || []), group.pk]
            const r = await akFetch('/core/users/' + akUser.pk + '/', 'PATCH', { groups: newGroups })
            if (r.status === 200) {
              entry.status = 'synced'
              log(`Legacy sync: added ${legacyEmail} to group ${targetGroup}`)
            } else {
              entry.status = 'error'
              entry.error = 'Authentik PATCH failed: ' + r.status
              report.errors.push(entry)
              continue
            }
          } else {
            entry.status = 'pending'
          }
          report.matched.push(entry)
        }

        if (!dryRun) invalidateCache()

        return json(res, 200, {
          dry_run: dryRun,
          summary: {
            to_sync: report.matched.length,
            already_ok: report.already_ok.length,
            not_found: report.not_found.length,
            errors: report.errors.length,
          },
          ...report,
        })
      } catch (e) {
        if (conn) try { await conn.end() } catch {}
        log('Legacy sync error:', e.message)
        return json(res, 502, { error: 'Legacy sync error: ' + e.message })
      }
    }

    return json(res, 404, { error: 'Unknown auth endpoint' })
  } catch (e) {
    log('Auth error:', e.message)
    return json(res, 502, { error: 'Auth error: ' + e.message })
  }
}

module.exports = { handle, CAPABILITIES, OPS_GROUPS }
