'use strict'
/**
 * store-pg.js — magasin Postgres DURABLE et NORMALISÉ du système de communication omnichannel (comme les tickets).
 *
 * Phase 2 (normalisé) :
 *   comms.conversations : 1 ligne / conversation = ENTÊTE (colonnes indexées : token, thread_id, email,
 *     customer, queue, status, last_activity) + `meta` jsonb (= la conv SANS ses messages : readBy, draft,
 *     notes, triage, assistants, ticketState, linkedTickets…). Lossless.
 *   comms.messages : 1 ligne / message (colonnes indexées : conv_token, gmail_id, ts, sender…) + `data` jsonb
 *     (= le message complet, lossless). PK (conv_token, id).
 *
 * Postgres MAÎTRE : le hub charge la Map depuis Postgres au boot (cf. conversation.js). Le JSON reste écrit
 * en double comme sauvegarde / rollback. Migration auto depuis l'ancien schéma Phase 1 (colonne `data` sur
 * conversations) : on supprime l'ancienne table (les données sont réamorcées depuis la Map/JSON).
 *
 * Réutilise la connexion Postgres ERPNext (cf. address-db.js). Tout est BEST-EFFORT (try/catch) → Postgres
 * down ⇒ `available()` false ⇒ le hub continue sur le JSON. S'inspire des stores Chatwoot/FreeScout.
 */
const { Pool } = require('pg')
const { log } = require('./helpers')

let _pool = null
let _available = false

function pool () {
  if (!_pool) {
    _pool = new Pool({
      host: process.env.COMMS_DB_HOST || process.env.ADDR_DB_HOST || 'erpnext-db-1',
      port: +(process.env.COMMS_DB_PORT || process.env.ADDR_DB_PORT || 5432),
      user: process.env.COMMS_DB_USER || process.env.ADDR_DB_USER || 'postgres',
      password: process.env.COMMS_DB_PASS || process.env.ADDR_DB_PASS || '123',
      database: process.env.COMMS_DB_NAME || process.env.ADDR_DB_NAME || '_eb65bdc0c4b1b2d6',
      max: 5, idleTimeoutMillis: 30000, connectionTimeoutMillis: 6000,
    })
    _pool.on('error', (e) => log('comms-pg pool error:', e.message))
  }
  return _pool
}

const DDL_CONV = `CREATE TABLE IF NOT EXISTS comms.conversations (
  token text PRIMARY KEY, channel text, phone text, email text, customer text, customer_name text,
  thread_id text, subject text, queue text, assignee text, status text,
  last_activity timestamptz, created_at timestamptz, expires_at timestamptz, noise boolean DEFAULT false,
  meta jsonb NOT NULL, updated_at timestamptz DEFAULT now())`
const DDL_MSG = `CREATE TABLE IF NOT EXISTS comms.messages (
  conv_token text NOT NULL, id text NOT NULL, ord int,
  sender text, from_name text, from_email text, to_email text, via text, agent text, gmail_id text,
  ts timestamptz, email_date timestamptz, data jsonb NOT NULL, updated_at timestamptz DEFAULT now(),
  PRIMARY KEY (conv_token, id))`
const DDL_IDX = [
  `CREATE INDEX IF NOT EXISTS conv_thread_idx   ON comms.conversations (thread_id)`,
  `CREATE INDEX IF NOT EXISTS conv_email_idx    ON comms.conversations (email)`,
  `CREATE INDEX IF NOT EXISTS conv_customer_idx ON comms.conversations (customer)`,
  `CREATE INDEX IF NOT EXISTS conv_queue_idx    ON comms.conversations (queue)`,
  `CREATE INDEX IF NOT EXISTS conv_status_idx   ON comms.conversations (status)`,
  `CREATE INDEX IF NOT EXISTS conv_lastact_idx  ON comms.conversations (last_activity DESC)`,
  `CREATE INDEX IF NOT EXISTS msg_conv_idx   ON comms.messages (conv_token)`,
  `CREATE INDEX IF NOT EXISTS msg_gmail_idx  ON comms.messages (gmail_id)`,
  `CREATE INDEX IF NOT EXISTS msg_ts_idx     ON comms.messages (ts)`,
]

async function init () {
  try {
    await pool().query('CREATE SCHEMA IF NOT EXISTS comms')
    // Migration auto Phase 1 → Phase 2 : l'ancienne table avait une colonne `data` (conv entière). On la
    // supprime (les données reviennent via le réamorçage depuis la Map/JSON) puis on crée le schéma normalisé.
    const cols = await pool().query(`SELECT column_name FROM information_schema.columns WHERE table_schema='comms' AND table_name='conversations'`)
    const names = cols.rows.map(r => r.column_name)
    if (names.includes('data') && !names.includes('meta')) {
      await pool().query('DROP TABLE IF EXISTS comms.conversations')
      log('comms-pg: migration Phase 1→2 — ancienne table conversations supprimée (réamorçage depuis JSON)')
    }
    await pool().query(DDL_CONV)
    await pool().query(DDL_MSG)
    for (const q of DDL_IDX) await pool().query(q)
    // Recherche FLOUE (typo-tolérante) : pg_trgm + index trigram GIN (nom client, courriel, sujet, corps des messages).
    try {
      await pool().query('CREATE EXTENSION IF NOT EXISTS pg_trgm')
      await pool().query(`CREATE INDEX IF NOT EXISTS conv_name_trgm  ON comms.conversations USING gin (customer_name gin_trgm_ops)`)
      await pool().query(`CREATE INDEX IF NOT EXISTS conv_email_trgm ON comms.conversations USING gin (email gin_trgm_ops)`)
      await pool().query(`CREATE INDEX IF NOT EXISTS conv_subj_trgm  ON comms.conversations USING gin (subject gin_trgm_ops)`)
      await pool().query(`CREATE INDEX IF NOT EXISTS msg_text_trgm   ON comms.messages USING gin ((data->>'text') gin_trgm_ops)`)
      log('comms-pg: recherche floue prête (pg_trgm + index trigram)')
    } catch (e) { log('comms-pg: pg_trgm indisponible (recherche exacte seulement):', e.message) }
    _available = true
    log('comms-pg: schéma normalisé prêt (comms.conversations + comms.messages)')
  } catch (e) {
    _available = false
    log('comms-pg: init échouée → repli JSON :', e.message)
  }
  return _available
}
function available () { return _available }

function toDate (v) { try { return v ? new Date(v) : null } catch { return null } }

const UP_CONV = `INSERT INTO comms.conversations
  (token,channel,phone,email,customer,customer_name,thread_id,subject,queue,assignee,status,last_activity,created_at,expires_at,noise,meta,updated_at)
  VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,now())
  ON CONFLICT (token) DO UPDATE SET channel=EXCLUDED.channel,phone=EXCLUDED.phone,email=EXCLUDED.email,
    customer=EXCLUDED.customer,customer_name=EXCLUDED.customer_name,thread_id=EXCLUDED.thread_id,subject=EXCLUDED.subject,
    queue=EXCLUDED.queue,assignee=EXCLUDED.assignee,status=EXCLUDED.status,last_activity=EXCLUDED.last_activity,
    created_at=EXCLUDED.created_at,expires_at=EXCLUDED.expires_at,noise=EXCLUDED.noise,meta=EXCLUDED.meta,updated_at=now()`
const UP_MSG = `INSERT INTO comms.messages
  (conv_token,id,ord,sender,from_name,from_email,to_email,via,agent,gmail_id,ts,email_date,data,updated_at)
  VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,now())
  ON CONFLICT (conv_token,id) DO UPDATE SET ord=EXCLUDED.ord,sender=EXCLUDED.sender,from_name=EXCLUDED.from_name,
    from_email=EXCLUDED.from_email,to_email=EXCLUDED.to_email,via=EXCLUDED.via,agent=EXCLUDED.agent,
    gmail_id=EXCLUDED.gmail_id,ts=EXCLUDED.ts,email_date=EXCLUDED.email_date,data=EXCLUDED.data,updated_at=now()`

// Upsert d'UNE conversation (entête + messages) dans une transaction.
async function upsert (conv) {
  if (!_available || !conv || !conv.token) return
  const client = await pool().connect()
  try {
    await client.query('BEGIN')
    const meta = { ...conv }; delete meta.messages
    await client.query(UP_CONV, [conv.token, conv.channel || null, conv.phone || null, conv.email || null,
      conv.customer || null, conv.customerName || null, conv.threadId || null, conv.lastSubject || conv.subject || null,
      conv.queue || null, conv.assignee || null, conv.status || null, toDate(conv.lastActivity), toDate(conv.createdAt),
      toDate(conv.expiresAt), !!conv.noise, JSON.stringify(meta)])
    const msgs = Array.isArray(conv.messages) ? conv.messages : []
    const ids = msgs.map(m => String(m.id))
    // retire les messages qui ne sont plus dans la conv (suppression/scission)
    await client.query('DELETE FROM comms.messages WHERE conv_token=$1 AND NOT (id = ANY($2::text[]))', [conv.token, ids.length ? ids : ['']])
    let ord = 0
    for (const m of msgs) {
      await client.query(UP_MSG, [conv.token, String(m.id), ord++, m.from || null, m.fromName || null, m.fromEmail || null,
        m.toEmail || null, m.via || null, m.agent || null, m.gmail_id || null, toDate(m.ts), toDate(m.emailDate), JSON.stringify(m)])
    }
    await client.query('COMMIT')
  } catch (e) {
    try { await client.query('ROLLBACK') } catch { /* */ }
    log('comms-pg upsert:', e.message)
  } finally { client.release() }
}

async function upsertAll (convs) {
  if (!_available || !Array.isArray(convs)) return 0
  let n = 0
  for (const c of convs) { await upsert(c); n++ }
  return n
}

async function remove (token) {
  if (!_available || !token) return
  try {
    await pool().query('DELETE FROM comms.messages WHERE conv_token=$1', [token])
    await pool().query('DELETE FROM comms.conversations WHERE token=$1', [token])
  } catch (e) { log('comms-pg remove:', e.message) }
}

// Reconstruit toutes les conversations (entête meta + messages ordonnés) → tableau de convs identiques à l'original.
async function loadAll () {
  if (!_available) return null
  try {
    const convs = await pool().query('SELECT token, meta FROM comms.conversations')
    const msgs = await pool().query('SELECT conv_token, data FROM comms.messages ORDER BY conv_token, ord')
    const byConv = new Map()
    for (const r of msgs.rows) { const a = byConv.get(r.conv_token) || []; a.push(r.data); byConv.set(r.conv_token, a) }
    return convs.rows.map(r => ({ ...r.meta, messages: byConv.get(r.token) || [] }))
  } catch (e) { log('comms-pg loadAll:', e.message); return null }
}

async function count () {
  if (!_available) return -1
  try { const r = await pool().query('SELECT count(*)::int AS n FROM comms.conversations'); return r.rows[0].n } catch { return -1 }
}
async function msgCount () {
  if (!_available) return -1
  try { const r = await pool().query('SELECT count(*)::int AS n FROM comms.messages'); return r.rows[0].n } catch { return -1 }
}

// Recherche FLOUE omnichannel (SQL) : nom client / courriel / sujet (exact ILIKE + trigram `<%` typo-tolérant)
// + corps des messages (ILIKE indexé trigram). Classé : correspondance directe d'abord, puis similarité, puis récence.
async function search (q, limit = 30) {
  if (!_available) return null // PG indispo → l'appelant fera un repli mémoire
  const term = String(q || '').trim()
  if (!term) return []
  const like = '%' + term.replace(/[%_\\]/g, '') + '%'
  try {
    const r = await pool().query(`
      SELECT c.token, c.customer_name, c.email, c.subject, c.channel, c.queue, c.status, c.last_activity,
        GREATEST(word_similarity($1, coalesce(c.customer_name,'')), word_similarity($1, coalesce(c.email,'')), word_similarity($1, coalesce(c.subject,''))) AS score,
        (c.customer_name ILIKE $2 OR c.email ILIKE $2 OR c.subject ILIKE $2) AS direct,
        (SELECT m.data->>'text' FROM comms.messages m WHERE m.conv_token=c.token AND (m.data->>'text') ILIKE $2 ORDER BY m.ts DESC LIMIT 1) AS msg_hit
      FROM comms.conversations c
      WHERE c.customer_name ILIKE $2 OR c.email ILIKE $2 OR c.subject ILIKE $2
         OR $1 <% coalesce(c.customer_name,'') OR $1 <% coalesce(c.email,'') OR $1 <% coalesce(c.subject,'')
         OR EXISTS (SELECT 1 FROM comms.messages m WHERE m.conv_token=c.token AND (m.data->>'text') ILIKE $2)
      ORDER BY direct DESC, score DESC, c.last_activity DESC NULLS LAST
      LIMIT $3`, [term, like, limit])
    return r.rows.map(x => ({
      token: x.token, customerName: x.customer_name, email: x.email, subject: x.subject,
      channel: x.channel, queue: x.queue, status: x.status, lastActivity: x.last_activity,
      score: Number(x.score || 0), direct: !!x.direct,
      snippet: x.msg_hit ? String(x.msg_hit).replace(/\s+/g, ' ').slice(0, 140) : null,
    }))
  } catch (e) { log('comms-pg search:', e.message); return null }
}

module.exports = { init, available, upsert, upsertAll, remove, loadAll, count, msgCount, search }
