'use strict'
// ─────────────────────────────────────────────────────────────────────────────
// Structured Frappe/ERPNext REST wrapper.
//
// Wraps helpers.erpFetch with:
//   - URL building (encodeURIComponent + JSON.stringify of filters/fields goes away)
//   - Consistent error shape ({ ok, status, data, error })
//   - v16 "Field not permitted" soft-retry (strip blocked fields and try once more)
//   - Batch label resolution (replace fetched-from links without violating v16)
//
// Pattern across the codebase before this:
//   const fields = JSON.stringify(['name', 'customer_name'])
//   const filters = JSON.stringify([['status', '=', 'open']])
//   const r = await erpFetch(`/api/resource/Dispatch%20Job?fields=${encodeURIComponent(fields)}&filters=${encodeURIComponent(filters)}&limit_page_length=50`)
//   const rows = r.status === 200 && Array.isArray(r.data?.data) ? r.data.data : []
//
// Pattern after this:
//   const rows = await erp.list('Dispatch Job', {
//     fields: ['name', 'customer_name'],
//     filters: [['status', '=', 'open']],
//     limit: 50,
//   })
// ─────────────────────────────────────────────────────────────────────────────

const { erpFetch, log } = require('./helpers')

// URL-encode a doctype name for /api/resource/ — spaces become %20, etc.
function encDocType (dt) { return encodeURIComponent(dt) }
function encName (n)     { return encodeURIComponent(n) }

// Build /api/resource/<Doctype>[?...] URL with list params
function listUrl (doctype, { fields, filters, orderBy, limit = 20, start = 0 } = {}) {
  const q = []
  if (fields && fields.length)  q.push('fields='  + encodeURIComponent(JSON.stringify(fields)))
  if (filters && filters.length) q.push('filters=' + encodeURIComponent(JSON.stringify(filters)))
  if (orderBy) q.push('order_by=' + encodeURIComponent(orderBy))
  if (limit != null)  q.push('limit_page_length=' + limit)
  if (start)          q.push('limit_start=' + start)
  return `/api/resource/${encDocType(doctype)}${q.length ? '?' + q.join('&') : ''}`
}

// Extract a human-readable message from Frappe error responses
function errorMessage (r) {
  if (!r) return 'no response'
  if (r.error) return r.error
  const d = r.data
  if (!d) return `HTTP ${r.status}`
  return d.exception || d._error_message || d.message || d._server_messages || `HTTP ${r.status}`
}

// Detect the v16 "Field not permitted in query: <fieldname>" error.
// Returns the offending field name, or null.
function fieldNotPermitted (r) {
  const msg = [r?.data?.exception, r?.data?._error_message, r?.data?._server_messages]
    .filter(Boolean).join(' ')
  const m = msg.match(/Field not permitted in query:\s*([a-zA-Z0-9_]+)/)
  return m ? m[1] : null
}

// ═════════════════════════════════════════════════════════════════════════
// Core CRUD
// ═════════════════════════════════════════════════════════════════════════

async function get (doctype, name, { fields } = {}) {
  let url = `/api/resource/${encDocType(doctype)}/${encName(name)}`
  if (fields && fields.length) url += `?fields=${encodeURIComponent(JSON.stringify(fields))}`
  const r = await erpFetch(url)
  if (r.status !== 200 || !r.data?.data) return null
  return r.data.data
}

// Mémo PROCESSUS des champs refusés par v16, PAR doctype. v16 refuse certains champs (ex. Dispatch Job.customer_name/service_type) ;
// sans mémo, CHAQUE liste repartait à zéro = jusqu'à 3 allers-retours ERPNext (2 échecs « Field not permitted » avant succès).
// Après la 1re découverte on les retire d'emblée → 1 seul aller-retour. Stable car même token (perms doctype constantes).
const _blockedFields = new Map() // doctype -> Set(champ refusé)
async function list (doctype, opts = {}) {
  // Retire d'emblée les champs déjà connus comme refusés (mémo), puis réessaie pour en découvrir de nouveaux.
  const known = _blockedFields.get(doctype)
  let fields = opts.fields ? (known ? opts.fields.filter(f => !known.has(f)) : [...opts.fields]) : null
  const dropped = []
  for (let attempt = 0; attempt < 5; attempt++) {
    const url = listUrl(doctype, { ...opts, fields })
    const r = await erpFetch(url)
    if (r.status === 200 && Array.isArray(r.data?.data)) return r.data.data
    const bad = fieldNotPermitted(r)
    if (bad && fields && fields.includes(bad)) {
      let set = _blockedFields.get(doctype); if (!set) { set = new Set(); _blockedFields.set(doctype, set) }
      if (!set.has(bad)) { set.add(bad); log(`erp.list ${doctype}: v16 refuse "${bad}" — mémorisé (plus redemandé)`) }
      dropped.push(bad)
      fields = fields.filter(f => f !== bad)
      continue
    }
    log(`erp.list ${doctype} failed: ${errorMessage(r)}`)
    return []
  }
  if (dropped.length) log(`erp.list ${doctype}: dropped ${dropped.join(',')} after retries`)
  return []
}

async function listRaw (doctype, opts = {}) {
  // Same as list() but returns { ok, rows, error } so callers that care about
  // failures can distinguish them from "empty result".
  let fields = opts.fields ? [...opts.fields] : null
  for (let attempt = 0; attempt < 5; attempt++) {
    const url = listUrl(doctype, { ...opts, fields })
    const r = await erpFetch(url)
    if (r.status === 200 && Array.isArray(r.data?.data)) return { ok: true, rows: r.data.data }
    const bad = fieldNotPermitted(r)
    if (bad && fields && fields.includes(bad)) { fields = fields.filter(f => f !== bad); continue }
    return { ok: false, rows: [], error: errorMessage(r), status: r.status }
  }
  return { ok: false, rows: [], error: 'too many field drops' }
}

async function create (doctype, body) {
  const r = await erpFetch(`/api/resource/${encDocType(doctype)}`, {
    method: 'POST', body: JSON.stringify(body),
  })
  if (r.status >= 400) { const error = errorMessage(r); log(`erp.create ${doctype} échec [${r.status}]: ${String(error).slice(0, 200)}`); return { ok: false, error, status: r.status } }
  return { ok: true, data: r.data?.data, name: r.data?.data?.name }
}

async function update (doctype, name, body) {
  const r = await erpFetch(`/api/resource/${encDocType(doctype)}/${encName(name)}`, {
    method: 'PUT', body: JSON.stringify(body),
  })
  if (r.status >= 400) { const error = errorMessage(r); log(`erp.update ${doctype}/${name} échec [${r.status}]: ${String(error).slice(0, 200)}`); return { ok: false, error, status: r.status } }
  return { ok: true, data: r.data?.data }
}

async function remove (doctype, name) {
  const r = await erpFetch(`/api/resource/${encDocType(doctype)}/${encName(name)}`, { method: 'DELETE' })
  if (r.status >= 400) { const error = errorMessage(r); log(`erp.remove ${doctype}/${name} échec [${r.status}]: ${String(error).slice(0, 200)}`); return { ok: false, error, status: r.status } }
  return { ok: true }
}

// ═════════════════════════════════════════════════════════════════════════
// Convenience helpers
// ═════════════════════════════════════════════════════════════════════════

// Fetch a batch of records by `name`, returning a { name: record } map.
// One query, no N+1. Used to resolve display labels after a list() that had
// link fields (e.g. customer → customer_name).
async function getMany (doctype, names, { fields } = {}) {
  if (!names || !names.length) return {}
  const ids = [...new Set(names.filter(Boolean))]
  if (!ids.length) return {}
  const rows = await list(doctype, {
    filters: [['name', 'in', ids]],
    fields: fields || ['name'],
    limit: Math.max(ids.length, 50),
  })
  const map = {}
  for (const r of rows) map[r.name] = r
  return map
}

// Fill link-label fields on a list of records without tripping v16's
// "Field not permitted in query: <name>" for fetched fields.
//
//   await erp.hydrateLabels(jobs, {
//     customer: {
//       doctype: 'Customer', out: 'customer_name',
//       fields: ['name', 'customer_name'],
//     },
//     service_location: {
//       doctype: 'Service Location', out: 'service_location_name',
//       fields: ['name', 'address_line_1', 'city'],
//       format: loc => [loc.address_line_1, loc.city].filter(Boolean).join(', ') || loc.name,
//     },
//   })
//
// Each config:
//   linkField → doctype: doctype to look up
//                out:     field to write on the record (defaults to `<linkField>_name`)
//                fields:  fields to pull (must include 'name')
//                format:  fn(linkedRow) → string (defaults to row[fields[1]] || row.name)
async function hydrateLabels (records, configs) {
  const linkFields = Object.keys(configs)

  // 1. Collect unique ids per link field
  const idsByField = {}
  for (const lf of linkFields) {
    idsByField[lf] = [...new Set(records.map(r => r[lf]).filter(Boolean))]
  }

  // 2. Fetch in parallel
  const entries = await Promise.all(linkFields.map(async lf => {
    const cfg = configs[lf]
    if (!idsByField[lf].length) return [lf, {}]
    const rows = await list(cfg.doctype, {
      filters: [['name', 'in', idsByField[lf]]],
      fields:  cfg.fields || ['name'],
      limit:   Math.max(idsByField[lf].length, 50),
    })
    const fmt = cfg.format || (row => row[cfg.fields && cfg.fields[1]] || row.name)
    const map = {}
    for (const row of rows) map[row.name] = fmt(row)
    return [lf, map]
  }))

  // 3. Write onto records
  const mapsByField = Object.fromEntries(entries)
  for (const r of records) {
    for (const lf of linkFields) {
      const out = configs[lf].out || (lf + '_name')
      r[out] = mapsByField[lf][r[lf]] || r[lf] || ''
    }
  }
  return records
}

// ═════════════════════════════════════════════════════════════════════════
// Escape hatch
// ═════════════════════════════════════════════════════════════════════════

// Raw passthrough for callers that need something outside this wrapper
// (e.g. custom Frappe method calls, reports, non-resource endpoints).
const raw = erpFetch

module.exports = {
  get, list, listRaw, create, update, remove,
  getMany, hydrateLabels,
  raw,
  // Useful internals if someone wants to build their own URL
  listUrl, errorMessage, fieldNotPermitted,
}
