'use strict'
/**
 * staff-agent.js — Copilote OPS en langage naturel (Gemini function-calling), surface STAFF.
 *
 * BUT (parité UI↔NL) : le langage naturel peut faire ce que fait l'interface visuelle, en exposant
 * les ACTIONS EXISTANTES comme « outils » function-calling. NL et UI partagent UNE SEULE couche
 * d'action (les routes hub / fonctions que l'UI appelle déjà) → couverture(NL) = couverture(UI).
 *
 * RÉUTILISE l'existant (cf feedback_reuse_shared_components) :
 *   - registre d'outils = services/targo-hub/lib/agent-tools.json (audience 'staff'),
 *   - runtime Gemini function-calling = lib/ai.js `chat({tools, wantMessage})` (même boucle que
 *     lib/agent.js répondeur SMS et lib/roster-assistant.js copilote roster),
 *   - couche d'action = les routes hub EXISTANTES (roster.js/dispatch.js/conversation.js), appelées
 *     en LOOPBACK autorisé (jeton de service + identité x-authentik-email) — zéro logique réécrite.
 *
 * SÉCURITÉ (cf feedback_ppa_double_charge_incident — une action auto peut coûter cher) :
 *   - outils LECTURE : s'exécutent librement pendant le PLAN (résolution d'entités, aucun effet).
 *   - outils ÉCRITURE/conséquents : NE s'exécutent PAS via le modèle. Ils sont mis en attente
 *     (staged) avec un APERÇU. L'exécution réelle passe par POST /staff-agent/run APRÈS confirmation
 *     humaine explicite (le modèle propose ; l'humain dispose). Pas d'endpoint arbitraire : /run mappe
 *     type → exécuteur côté serveur (liste blanche), re-valide les permissions (parité usePermissions
 *     via auth.effectiveCapabilities) et agit AU NOM de l'utilisateur connecté (x-authentik-email).
 *
 * Routes :
 *   POST /staff-agent        { text }              → { ok, reply, actions:[{id,type,title,preview,params,permission,needs_confirmation}], transcript }
 *   POST /staff-agent/run    { actions }           → { ok, results:[{type,ok,message|error}] }
 *   GET  /staff-agent/tools                         → { tools } (inventaire — parité/complétude)
 */
const cfg = require('./config')
const { log, json, parseBody } = require('./helpers')
const { toolsForModel, toolsByAudience } = require('./agent')
const erp = require('./erp')

// ── Registre : outils STAFF (audience 'staff') depuis le registre UNIQUE agent-tools.json ──
const STAFF_TOOLS_RAW = toolsByAudience('staff')
const STAFF_TOOLS = toolsForModel(STAFF_TOOLS_RAW) // envoyés au modèle : {type, function} seulement
const META = {} // name → { mode, permission, title }
for (const t of STAFF_TOOLS_RAW) META[t.function.name] = { mode: t.mode || 'read', permission: t.permission || null, title: t.title || t.function.name }

// require paresseux (évite les cycles + n'alourdit pas le boot) — mêmes modules que l'UI appelle.
const roster = () => require('./roster')
const dispatch = () => require('./dispatch')

function todayET () { return new Date().toLocaleDateString('en-CA', { timeZone: 'America/Toronto' }) }

// ── Loopback AUTORISÉ vers les routes hub EXISTANTES ─────────────────────────
// Réutilise EXACTEMENT les handlers que le front appelle (couche d'action unique). On imite le proxy
// nginx /ops/hub : Authorization = jeton de service + x-authentik-email = utilisateur connecté (→
// attribution, suivi, et vérifs d'identité des handlers). Pas d'endpoint fourni par le client : seuls
// les chemins codés dans EXECUTORS sont atteignables.
const PORT = cfg.PORT || 3300
async function hubCall (method, path, body, email) {
  const headers = { 'Content-Type': 'application/json' }
  if (process.env.HUB_SERVICE_TOKEN) headers.Authorization = 'Bearer ' + process.env.HUB_SERVICE_TOKEN
  if (email) headers['x-authentik-email'] = email
  const opt = { method, headers }
  if (body != null && method !== 'GET') opt.body = JSON.stringify(body)
  const r = await fetch(`http://127.0.0.1:${PORT}${path}`, opt)
  let data = null
  try { data = await r.json() } catch { /* corps non-JSON */ }
  return { status: r.status, ok: r.ok, data: data || {} }
}
// Un résultat hub → { ok, message } normalisé (les routes renvoient {ok:false,error} ou {error}).
function hubResult (r, okMsg) {
  const ok = r.ok && r.data && r.data.ok !== false && !r.data.error
  return { ok, message: ok ? okMsg : ((r.data && r.data.error) || `échec (HTTP ${r.status})`), status: r.status, data: r.data }
}

async function resolveTechName (techId) {
  try { const techs = await roster().fetchTechnicians(); const t = techs.find(x => x.id === techId || x.name === techId); return t ? t.name : techId }
  catch { return techId }
}

// ── Outils LECTURE (exécutés pendant le PLAN — sans effet de bord) ───────────
async function read_list_technicians ({ query, skill } = {}) {
  let techs
  try { techs = await roster().fetchTechnicians() } catch (e) { return { error: 'techniciens indisponibles : ' + e.message } }
  const q = String(query || '').trim().toLowerCase()
  const sk = String(skill || '').trim().toLowerCase()
  let out = techs || []
  if (q) out = out.filter(t => String(t.name || '').toLowerCase().includes(q) || String(t.id || '').toLowerCase().includes(q))
  if (sk) out = out.filter(t => (t.skills || []).some(s => String(s).toLowerCase().includes(sk)))
  return { count: out.length, technicians: out.slice(0, 40).map(t => ({ id: t.id, name: t.name, status: t.status, skills: t.skills || [] })) }
}
async function read_get_job ({ job } = {}) {
  if (!job) return { error: 'job requis' }
  try {
    const d = await erp.get('Dispatch Job', job, { fields: ['name', 'subject', 'status', 'assigned_tech', 'scheduled_date', 'start_time', 'duration_h', 'priority', 'customer_name', 'service_location', 'job_type', 'booking_status'] })
    if (!d || !d.name) return { error: 'job introuvable : ' + job }
    return { job: d.name, subject: d.subject, status: d.status, assigned_tech: d.assigned_tech || null, scheduled_date: d.scheduled_date || null, start_time: d.start_time || null, duration_h: d.duration_h, priority: d.priority, customer_name: d.customer_name || '', service_location: d.service_location || '', job_type: d.job_type, booking_status: d.booking_status || '' }
  } catch (e) { return { error: e.message } }
}
async function read_find_slot ({ duration_h, skill, after_date } = {}) {
  try {
    const slots = await dispatch().suggestSlots({ duration_h: Number(duration_h) || 1, skill: skill || '', after_date: after_date || todayET(), limit: 5 })
    return { count: (slots || []).length, slots: (slots || []).slice(0, 5) }
  } catch (e) { return { error: e.message } }
}
const READERS = { list_technicians: read_list_technicians, get_job: read_get_job, find_slot: read_find_slot }

// ── Jours de la semaine — clés du weekly_schedule (= DOW_KEYS de roster.js) ──
const DAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
const DAY_FR = { mon: 'Lun', tue: 'Mar', wed: 'Mer', thu: 'Jeu', fri: 'Ven', sat: 'Sam', sun: 'Dim' }
const DAY_ALIASES = {
  mon: 'mon', tue: 'tue', wed: 'wed', thu: 'thu', fri: 'fri', sat: 'sat', sun: 'sun',
  monday: 'mon', tuesday: 'tue', wednesday: 'wed', thursday: 'thu', friday: 'fri', saturday: 'sat', sunday: 'sun',
  lun: 'mon', mar: 'tue', mer: 'wed', jeu: 'thu', ven: 'fri', sam: 'sat', dim: 'sun',
  lundi: 'mon', mardi: 'tue', mercredi: 'wed', jeudi: 'thu', vendredi: 'fri', samedi: 'sat', dimanche: 'sun',
}
function normDays (days) {
  const set = new Set()
  for (const d of (Array.isArray(days) ? days : [])) { const k = DAY_ALIASES[String(d || '').trim().toLowerCase()]; if (k) set.add(k) }
  return DAY_KEYS.filter(k => set.has(k)) // ordre lun→dim, dédupliqué
}
function normTime (t, dflt = '') {
  const m = String(t || '').trim().match(/^(\d{1,2})(?:\s*[:hH]\s*(\d{2}))?/) // "8", "8:00", "8h", "16h00"
  if (!m) return dflt
  return String(Math.min(23, parseInt(m[1], 10))).padStart(2, '0') + ':' + (m[2] || '00')
}
function buildWeeklySchedule (days, start, end) {
  const st = normTime(start), en = normTime(end)
  const sched = {}
  for (const k of normDays(days)) sched[k] = { start: st, end: en }
  return sched
}

// ── Outils ÉCRITURE — { permission, build(params)→{title,preview,params[,severity]}, exec(params,email) } ──
// build : mis en attente pendant le plan (aperçu). exec : exécution réelle après confirmation (via /run),
// chaque exec = fine enveloppe sur une route/fonction hub EXISTANTE.
const WRITES = {
  create_job: {
    permission: 'create_jobs',
    async build (p) {
      const params = { subject: String(p.subject || '').trim(), customer_id: p.customer_id || '', service_location: p.service_location || '', priority: p.priority || '', job_type: p.job_type || '', notes: p.notes || '', auto_assign: p.auto_assign !== false }
      const preview = `« ${params.subject || 'Intervention'} »${params.job_type ? ' · ' + params.job_type : ''}${params.priority ? ' · priorité ' + params.priority : ''}${params.customer_id ? ' · client ' + params.customer_id : ''} · ${params.auto_assign ? 'assignation auto' : 'non assigné'}`
      return { title: META.create_job.title, preview, params }
    },
    async exec (p) {
      const r = await dispatch().agentCreateDispatchJob({ customer_id: p.customer_id || '', service_location: p.service_location || '', subject: p.subject, priority: p.priority || undefined, job_type: p.job_type || undefined, notes: p.notes || '', auto_assign: p.auto_assign })
      return { ok: !!(r && r.success), message: (r && r.message) || 'job créé', job_id: r && r.job_id, assigned_tech: r && r.assigned_tech }
    },
  },
  assign_tech: {
    permission: 'assign_jobs',
    async build (p) {
      const name = await resolveTechName(p.tech_id)
      const params = { job: p.job, tech: p.tech_id, date: p.date || '', start: p.start || '' }
      return { title: META.assign_tech.title, preview: `${p.job} → ${name} (${p.tech_id})${p.date ? ' · ' + p.date : ''}${p.start ? ' à ' + p.start : ''}`, params }
    },
    async exec (p, email) {
      const body = { job: p.job, tech: p.tech }
      if (p.date) body.date = p.date
      if (p.start) body.start = p.start
      return hubResult(await hubCall('POST', '/roster/assign-job', body, email), `${p.job} assigné à ${p.tech}`)
    },
  },
  add_assistant: {
    permission: 'assign_jobs',
    async build (p) {
      const name = p.tech_name || await resolveTechName(p.tech_id)
      return { title: META.add_assistant.title, preview: `Renfort sur ${p.job} : ${name} (${p.tech_id})`, params: { job: p.job, tech_id: p.tech_id, tech_name: name } }
    },
    async exec (p, email) {
      return hubResult(await hubCall('POST', '/roster/job/team', { job: p.job, add: { tech_id: p.tech_id, tech_name: p.tech_name || '' } }, email), `Assistant ${p.tech_id} ajouté à ${p.job}`)
    },
  },
  set_job_status: {
    permission: 'assign_jobs',
    async build (p) {
      return { title: META.set_job_status.title, preview: `${p.job} → statut « ${p.status} »`, params: { job: p.job, status: p.status }, severity: p.status === 'Cancelled' ? 'high' : 'normal' }
    },
    async exec (p, email) {
      return hubResult(await hubCall('POST', '/roster/job/update', { job: p.job, patch: { status: p.status } }, email), `${p.job} → ${p.status}`)
    },
  },
  reschedule_job: {
    permission: 'assign_jobs',
    async build (p) {
      return { title: META.reschedule_job.title, preview: `${p.job} reporté au ${p.date}${p.start ? ' à ' + p.start : ''}`, params: { job: p.job, date: p.date, start: p.start || '' } }
    },
    async exec (p, email) {
      // Conserve le tech assigné si possible (re-place l'heure via /roster/assign-job) ; sinon change la date via /roster/job/update.
      let tech = null
      try { const d = await erp.get('Dispatch Job', p.job, { fields: ['assigned_tech'] }); tech = d && d.assigned_tech } catch { /* fallback ci-dessous */ }
      if (tech) {
        const body = { job: p.job, tech, date: p.date }
        if (p.start) body.start = p.start
        return hubResult(await hubCall('POST', '/roster/assign-job', body, email), `${p.job} reporté au ${p.date}`)
      }
      return hubResult(await hubCall('POST', '/roster/job/update', { job: p.job, patch: { scheduled_date: p.date } }, email), `${p.job} reporté au ${p.date}`)
    },
  },
  create_recurring_shift: {
    permission: 'assign_jobs',
    async build (p) {
      const days = normDays(p.days)
      const schedule = buildWeeklySchedule(p.days, p.start, p.end)
      const name = await resolveTechName(p.technicien_id)
      const st = normTime(p.start), en = normTime(p.end)
      const worked = days.map(k => DAY_FR[k]).join(', ') || '(aucun jour)'
      const off = DAY_KEYS.filter(k => !days.includes(k)).map(k => DAY_FR[k]).join(', ')
      const preview = `${name} (${p.technicien_id}) : ${st}–${en} le ${worked}${off ? ` · repos ${off}` : ''}`
      // schedule pré-calculé porté dans les params → /run le pousse tel quel sur la route weekly-schedule.
      return { title: META.create_recurring_shift.title, preview, params: { technicien_id: p.technicien_id, schedule } }
    },
    async exec (p, email) {
      if (!p.schedule || typeof p.schedule !== 'object' || !Object.keys(p.schedule).length) return { ok: false, message: 'horaire vide (aucun jour)' }
      return hubResult(await hubCall('POST', `/roster/technician/${encodeURIComponent(p.technicien_id)}/weekly-schedule`, { schedule: p.schedule }, email), 'Horaire récurrent enregistré (quarts matérialisés au prochain cycle)')
    },
  },
  follow_doc: {
    permission: null, // suivi = personnel/bas risque → tout staff authentifié
    async build (p) {
      const follow = p.follow !== false
      return { title: META.follow_doc.title, preview: `${follow ? 'Suivre' : 'Ne plus suivre'} ${p.doctype} ${p.name}`, params: { doctype: p.doctype, name: p.name, follow } }
    },
    async exec (p, email) {
      return hubResult(await hubCall('POST', '/conversations/follow', { doctype: p.doctype, name: p.name, follow: p.follow !== false }, email), (p.follow !== false) ? 'Suivi activé' : 'Suivi retiré')
    },
  },
}

// ── Runtime Gemini (function-calling) — même config/boucle que lib/agent.js & roster-assistant.js ──
async function geminiChat (messages) {
  const msg = await require('./ai').chat({ task: 'nl', messages, tools: STAFF_TOOLS, maxTokens: 900, temperature: 0.1, wantMessage: true })
  return { choices: [{ message: msg }] }
}

function systemPrompt (email) {
  return `Tu es le COPILOTE OPS (dispatch/planification) de Gigafibre/TARGO, fournisseur Internet/TV/téléphonie au Québec.
Aujourd'hui = ${todayET()}. Tu agis AU NOM de l'utilisateur connecté${email ? ` (${email})` : ''}.
Tu transformes une commande en langage naturel en APPELS D'OUTILS.
RÈGLES :
- Utilise d'abord les outils de LECTURE (list_technicians, get_job, find_slot) pour RÉSOUDRE les entités. Résous toujours un technicien nommé (« Simon ») en tech_id via list_technicians avant d'agir.
- Les outils d'ÉCRITURE (create_job, assign_tech, add_assistant, set_job_status, reschedule_job, create_recurring_shift, follow_doc) NE S'EXÉCUTENT PAS tout de suite : ils sont PROPOSÉS puis CONFIRMÉS par l'utilisateur. Chaque appel te renvoie un APERÇU (staged=true). N'affirme JAMAIS qu'une action est faite ; annonce ce qui SERA fait après confirmation.
- Horaire/quart RÉCURRENT : days parmi mon,tue,wed,thu,fri,sat,sun. « jours de semaine » = mon,tue,wed,thu,fri ; retire les exceptions (« sauf le vendredi » → mon,tue,wed,thu). « fin de semaine » = sat,sun. Convertis « 8-16h » en start 08:00 / end 16:00. Il FAUT un technicien : si aucun n'est nommé, DEMANDE-le.
- S'il manque une information essentielle (quel job ? quel tech ? quelle date ?), pose UNE question courte au lieu d'appeler un outil d'écriture.
- Réponds en français, bref. Après avoir proposé des actions, résume-les en une phrase et invite à confirmer.`
}

async function execToolPlan (name, args, ctx) {
  const meta = META[name]
  if (!meta) return { error: 'outil inconnu : ' + name }
  try {
    if (meta.mode === 'read') { const fn = READERS[name]; return fn ? await fn(args || {}) : { error: 'lecteur manquant : ' + name } }
    const w = WRITES[name]
    if (!w) return { error: 'exécuteur manquant : ' + name }
    const staged = await w.build(args || {}, ctx.email)
    const id = 'a' + (ctx.planned.length + 1)
    ctx.planned.push({ id, type: name, title: staged.title, preview: staged.preview, params: staged.params, permission: meta.permission || null, severity: staged.severity || 'normal', needs_confirmation: true })
    return { staged: true, requires_confirmation: true, title: staged.title, preview: staged.preview, note: "Action PROPOSÉE, NON exécutée. L'utilisateur doit la confirmer. Ne dis pas qu'elle est faite." }
  } catch (e) { return { error: String(e.message || e) } }
}

// PLAN : boucle function-calling. Les LECTURES s'exécutent (résolution), les ÉCRITURES sont mises en attente.
async function plan (text, email) {
  if (!cfg.AI_API_KEY) return { ok: false, error: "Le service IA n'est pas configuré (AI_API_KEY manquante)." }
  const planned = []
  const ctx = { email, planned }
  const messages = [{ role: 'system', content: systemPrompt(email) }, { role: 'user', content: String(text || '') }]
  let response
  try { response = await geminiChat(messages) } catch (e) { return { ok: false, error: 'IA : ' + e.message } }
  const cur = [...messages]
  for (let i = 0; i < 6; i++) {
    const msg = response.choices?.[0]?.message
    if (!msg) break
    const calls = msg.tool_calls // signal OpenAI-compat des appels d'outils (indépendant de finish_reason)
    if (!calls || !calls.length) break
    cur.push(msg)
    for (const tc of calls) {
      const args = (() => { try { return JSON.parse(tc.function.arguments || '{}') } catch { return {} } })()
      log(`[staff-agent] ${email || 'anon'} → ${tc.function.name}(${JSON.stringify(args)})`)
      const result = await execToolPlan(tc.function.name, args, ctx)
      cur.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result).slice(0, 6000) })
    }
    try { response = await geminiChat(cur) } catch (e) { return { ok: false, error: 'IA : ' + e.message } }
  }
  const reply = response.choices?.[0]?.message?.content || (planned.length ? 'Plan préparé — confirme pour exécuter.' : 'Aucune action détectée — reformule.')
  return { ok: true, reply, actions: planned, transcript: String(text || '') }
}

// RUN : exécute les actions CONFIRMÉES. type → exécuteur (liste blanche) + re-vérif permission + identité.
async function run (actions, email) {
  const list = Array.isArray(actions) ? actions.slice(0, 12) : []
  let caps = null
  try { caps = await require('./auth').effectiveCapabilities(email) } catch (e) { log('[staff-agent] perms indispo : ' + e.message) }
  const results = []
  for (const a of list) {
    const type = a && a.type
    const w = WRITES[type]
    if (!w) { results.push({ type, ok: false, error: 'action inconnue' }); continue }
    // Permissions (parité usePermissions) : appliquées si Authentik est branché (prod). En dev/aperçu
    // (pas de token → caps.configured=false) on n'enforce pas mais on journalise — le gate du hub
    // (jeton de service) garantit déjà que seul un membre du staff authentifié atteint /staff-agent.
    const need = w.permission
    if (need && caps && caps.configured) {
      const allowed = caps.is_superuser || (caps.capabilities && caps.capabilities[need] === true)
      if (!allowed) { results.push({ type, ok: false, denied: true, error: `permission requise : ${need}` }); continue }
    }
    try { results.push({ type, ...(await w.exec(a.params || {}, email)) }) }
    catch (e) { results.push({ type, ok: false, error: String(e.message || e) }) }
  }
  return { ok: results.length > 0 && results.every(r => r.ok), results }
}

async function handle (req, res, method, path) {
  const email = String(req.headers['x-authentik-email'] || '').toLowerCase()
  if (path === '/staff-agent' && method === 'POST') {
    const b = await parseBody(req)
    try { return json(res, 200, await plan(b.text || '', email)) }
    catch (e) { return json(res, 200, { ok: false, error: 'Erreur copilote : ' + (e.message || e) }) }
  }
  if (path === '/staff-agent/run' && method === 'POST') {
    const b = await parseBody(req)
    try { return json(res, 200, await run(b.actions, email)) }
    catch (e) { return json(res, 200, { ok: false, error: 'Erreur exécution : ' + (e.message || e) }) }
  }
  if (path === '/staff-agent/tools' && method === 'GET') {
    return json(res, 200, { tools: STAFF_TOOLS_RAW.map(t => ({ name: t.function.name, mode: t.mode || 'read', permission: t.permission || null, title: t.title || t.function.name, description: t.function.description })) })
  }
  return json(res, 404, { error: 'staff-agent: route inconnue ' + path })
}

module.exports = { handle, plan, run, buildWeeklySchedule, normDays, normTime, STAFF_TOOLS_RAW }
