/* ===================================================================
   KabelFlux v5 — Live-backend hydration
   PROJECTS is loaded from the backend on boot. Per-project detail
   (wires / connectors / nodes / bundles) is loaded on demand by
   kfxLoadAndMapProject() and held as React state in app.jsx — the
   old WIRES / CONNECTORS / NODES / BUNDLES globals are gone.
   =================================================================== */

/* Start empty so the shell renders during auth bootstrap.
   app.jsx swaps these for React state once kfxBootstrap() resolves. */
let PROJECTS = [];

/* IEC 60757 color codes for wire colors — real lookup tables, keep static */
const COLOR_HEX = {
  Red:'#cc2200', Black:'#222', White:'#dde2ee', Blue:'#1e4dcc', Green:'#2a8a2a',
  Yellow:'#e6c200', Orange:'#e07820', Brown:'#7a4220', Violet:'#7a1aaa',
  Gray:'#9aa3b8', Pink:'#e060a0', Drainwire:'#aaa'
};
const COLOR_CODE = {
  Red:'RD', Black:'BK', White:'WH', Blue:'BU', Green:'GN', Yellow:'YE',
  Orange:'OG', Brown:'BN', Violet:'VT', Gray:'GY', Pink:'PK', Drainwire:'DR'
};

/* Per-project content is loaded from the backend by
   kfxLoadAndMapProject() (see below) and threaded as props through
   app.jsx. No module-level WIRES / CONNECTORS / NODES / BUNDLES. */

/* ── Auth + fetch helpers (legacy iat_token key — same as /legacy) ── */
const KFX_API = `${window.location.protocol}//${window.location.host}/api`;

function kfxGetToken() {
  try { return localStorage.getItem('iat_token') || ''; }
  catch (e) { return ''; }
}
function kfxSetAuth(token, user) {
  try {
    localStorage.setItem('iat_token', token);
    localStorage.setItem('iat_user', JSON.stringify(user));
  } catch (e) {}
}
function kfxClearAuth() {
  try {
    localStorage.removeItem('iat_token');
    localStorage.removeItem('iat_user');
  } catch (e) {}
}

async function kfxFetch(path, opts = {}) {
  const token = kfxGetToken();
  const headers = Object.assign({}, opts.headers || {});
  if (token) headers['Authorization'] = 'Bearer ' + token;
  const hasBody = opts.body !== undefined && opts.body !== null;
  const isForm = hasBody && opts.body instanceof FormData;
  if (hasBody && !isForm && !headers['Content-Type']) {
    headers['Content-Type'] = 'application/json';
  }
  const r = await fetch(KFX_API + path, {
    method: opts.method || 'GET',
    headers,
    body: hasBody ? (isForm ? opts.body : JSON.stringify(opts.body)) : undefined,
  });
  const ct = (r.headers.get('content-type') || '').toLowerCase();
  let payload = null;
  if (ct.includes('application/json')) {
    try { payload = await r.json(); } catch (e) { payload = null; }
  } else {
    try { payload = await r.text(); } catch (e) { payload = null; }
  }
  if (!r.ok) {
    const message = (payload && (payload.detail || payload.message)) || (typeof payload === 'string' && payload) || r.statusText;
    const err = new Error(message);
    err.status = r.status;
    err.message = message;
    throw err;
  }
  return payload;
}

/* Relative-time formatter for project.modified — minimal, no deps. */
function kfxRelativeTime(iso) {
  if (!iso) return '';
  const t = Date.parse(iso);
  if (isNaN(t)) return String(iso);
  const diffSec = Math.max(0, (Date.now() - t) / 1000);
  if (diffSec < 60)     return Math.floor(diffSec) + 's ago';
  if (diffSec < 3600)   return Math.floor(diffSec / 60) + ' min ago';
  if (diffSec < 86400)  return Math.floor(diffSec / 3600) + ' h ago';
  if (diffSec < 172800) return 'Yesterday';
  if (diffSec < 604800) return Math.floor(diffSec / 86400) + ' d ago';
  return Math.floor(diffSec / 604800) + ' wk ago';
}

/* Map a backend project row onto the bundle's project shape. */
function kfxMapProject(p) {
  const blob = String((p.name || '') + ' ' + (p.description || ''));
  const m = blob.match(/HA\d{7}/i);
  const code = m ? m[0].toUpperCase() : ('P' + String(p.id).padStart(6, '0'));
  return {
    id: 'p' + p.id,
    realId: p.id,
    code,
    name: p.harness_name || p.name,
    revision: p.revision || 'A',
    wires: p.wires_count || 0,
    conns: p.connectors_count || 0,
    nodes: p.nodes_count || 0,
    modified: kfxRelativeTime(p.updated_at),
    owner: p.owner_name || '',
    pinned: false,
    status: p.shared ? 'active' : 'draft',
    company_name: p.company_name || '',
    _raw: p,
  };
}

async function kfxLoadAuthMe() {
  try {
    return await kfxFetch('/auth/me');
  } catch (e) {
    if (e.status === 401) return null;
    throw e;
  }
}

async function kfxLoadProjects() {
  const rows = await kfxFetch('/projects');
  return (rows || []).map(kfxMapProject);
}

/* ── Project detail (wires / connectors / nodes) loader + mappers ──
   The backend returns each row in its raw schema (snake_case, all
   strings for boolean-ish fields). These mappers translate to the
   shape the bundle's views expect (camelCase, real booleans). */

async function kfxLoadProject(realId) {
  return await kfxFetch('/projects/' + realId);
}

/* Connector library — the global catalog seeded by the backend (180 built-in
   parts + any user-added custom entries). Returned shape per backend row:
   { id, part_number, manufacturer, series, description, connector_type,
     pin_count, gender, symbol, termination, datasheet_url,
     mating_part_number, is_builtin, notes, created_at }
*/
async function kfxLoadConnectorLibrary() {
  return await kfxFetch('/connlib');
}

/* Map a backend connlib row to the shape the bundle's SymbolLibrary view
   expects: { id, family, name, pins, gender, shell, used, updated, tags,
              manufacturer, series, symbol, partNumber, _raw }
*/
function kfxMapLibraryRow(row) {
  const gender = (row.gender || '').toLowerCase();
  return {
    id:           row.part_number || ('LIB' + row.id),
    partNumber:   row.part_number || '',
    manufacturer: row.manufacturer || 'Other',
    series:       row.series || '',
    /* family key — lowercased + dashes — drives the existing tab filter.
       The user wanted manufacturer-grouped headers; we use manufacturer
       as the family axis so the existing kf-library-fam tabs become
       manufacturer tabs (AMASS / AMPHENOL / ...). */
    family:       (row.manufacturer || 'other').toLowerCase().replace(/\s+/g, '-'),
    name:         row.part_number || row.description || '(unnamed)',
    description:  row.description || '',
    connectorType:row.connector_type || '',
    pins:         row.pin_count || 0,
    /* The bundle's existing grid filter uses 'plug' / 'recep' / '—'.
       Backend uses 'male' / 'female'. Map across. */
    gender:       gender === 'male' ? 'plug' : gender === 'female' ? 'recep' : '—',
    shell:        row.series || '—',
    used:         0,                       /* per-project usage — TODO once we add it server-side */
    updated:      (row.created_at || '').slice(0, 10),
    symbol:       row.symbol || 'rectangular',
    termination:  row.termination || '',
    isBuiltin:    !!row.is_builtin,
    tags:         [row.series, row.termination].filter(Boolean),
    _raw:         row,
  };
}

/* kfxAuditProject moved into views-layout.jsx — see Analyse chip there. */

function kfxMapWire(w, idx) {
  const color = w.color || '';
  const code = COLOR_CODE[color] || color.slice(0, 2).toUpperCase();
  return {
    id: 'w' + w.id,
    realId: w.id,
    cavity: idx + 1,
    fromConn: w.from_connector_name || '',
    fromPin: w.from_pin || '',
    // When a wire terminates at a node, the backend's data model fills
    // EITHER to_connector_name with the node name OR (in older data) leaves
    // to_connector_name blank and only sets node_ref. Fall back to node_ref
    // so wires-through-nodes still draw a line. Same idea for the from side
    // (rare, but possible if a continuing segment originates from a node).
    toConn: w.to_connector_name || w.node_ref || '',
    toPin: w.to_pin || '',
    node_ref: w.node_ref || '',
    color,
    code,
    gauge: w.gauge || '',
    spec: w.wire_spec || '',
    length: parseInt(w.length, 10) || 0,
    shield: !!w.shield && w.shield !== '',
    twisted: !!w.twisted_pair && w.twisted_pair !== '',
    cable: w.cable_type || 'Single core',
    net: w.net || '',
    notes: w.notes || '',
    _raw: w,
  };
}

function kfxMapConnector(c) {
  const name = c.connector_name || ('J' + c.id);
  const gender = c.gender === 'male' ? 'M' : (c.gender === 'female' ? 'F' : '');
  return {
    id: name,
    realId: c.id,
    type: c.connector_type || '',
    gender,
    pins: c.pin_count || 0,
    x: c.x || 0,
    y: c.y || 0,
    label: name + (c.description ? ' — ' + c.description : ''),
    _raw: c,
  };
}

function kfxMapNode(n) {
  /* Backend columns are `label` and `node_type` — NOT `name`/`type`.
     The id MUST be the label so that wires referencing a node by name
     ("NODE_W2A") can resolve it via nodes.find(n => n.id === w.fromConn)
     in views-layout.jsx + views-other.jsx wire-path lookups. Earlier
     versions used n.name (which doesn't exist) and fell back to 'N<id>',
     which made every wire-through-node silently fail to draw. */
  const label = n.label || n.name || '';
  return {
    id:     label || ('N' + n.id),
    realId: n.id,
    type:   n.node_type || n.type || 'splice',
    x:      n.x || 0,
    y:      n.y || 0,
    label:  label,
    value:  n.value || '',
    _raw:   n,
  };
}

/* Backend has no geometric bundles — length_defs are length variants,
   not visual harness bundles. Until the backend ships real bundle
   geometry we return []; the layout view skips bundle rendering and
   the BOM "Length by bundle" chart just shows an empty bar list. */
function kfxMapBundles() { return []; }

async function kfxLoadAndMapProject(realId) {
  const raw = await kfxLoadProject(realId);
  return {
    raw,
    wires: (raw.wires || []).map(kfxMapWire),
    connectors: (raw.connectors || []).map(kfxMapConnector),
    nodes: (raw.nodes || []).map(kfxMapNode),
    bundles: kfxMapBundles(raw),
  };
}

async function kfxLogin(username, password) {
  const d = await kfxFetch('/auth/login', {
    method: 'POST',
    body: { username, password },
  });
  const user = {
    username: d.username,
    full_name: d.full_name,
    role: d.role,
    uid: d.uid || 0,
    company_id: d.company_id,
    company_name: d.company_name || '',
    logo_data: d.logo_data || '',
    email: d.email || '',
    job_title: d.job_title || '',
  };
  kfxSetAuth(d.token, user);
  return user;
}

async function kfxLogout() {
  try { await kfxFetch('/auth/logout', { method: 'POST' }); }
  catch (e) { /* best-effort */ }
  kfxClearAuth();
}

/* ── CRUD helpers: convert bundle-shape → backend-shape and call API.
   Used by views to add/edit/delete wires, connectors and nodes.
   After any successful mutation, the caller should re-run
   kfxLoadAndMapProject() so the UI stays consistent. ── */

function kfxWireToBackend(wire) {
  return {
    from_connector_name: wire.fromConn || '',
    from_pin:            wire.fromPin !== undefined && wire.fromPin !== null ? String(wire.fromPin) : '',
    to_connector_name:   wire.toConn || '',
    to_pin:              wire.toPin !== undefined && wire.toPin !== null ? String(wire.toPin) : '',
    connection_id:       wire.connection_id || '',
    net:                 wire.net || '',
    color:               wire.color || 'Black',
    gauge:               wire.gauge || '',
    wire_spec:           wire.spec || '',
    length:              wire.length !== undefined && wire.length !== null ? String(wire.length) : '',
    cable_type:          wire.cable || 'Single core',
    shield:              wire.shield ? 'Y' : '',
    twisted_pair:        wire.twisted ? 'Y' : '',
    notes:               wire.notes || '',
  };
}

function kfxConnectorToBackend(conn) {
  const gender = conn.gender === 'M' ? 'male' : conn.gender === 'F' ? 'female' : (conn.gender || 'male');
  return {
    connector_name: conn.id || conn.name || '',
    connector_type: conn.type || '',
    description:    conn.description || '',
    pin_count:      conn.pins || 0,
    gender,
    x:              conn.x || 100,
    y:              conn.y || 100,
  };
}

function kfxNodeToBackend(node) {
  /* Length goes to the backend as a plain string (e.g. "250" — unit is
     ignored server-side; the model column is TEXT). If the user picked
     inches we still send the numeric so the formboard render math works,
     and append a hint to description if it's not already there. */
  let length = node.length || '';
  return {
    node_type:   node.type || 'splice',
    label:       node.label || node.id || '',
    value:       node.value || '',
    part_number: node.part_number || '',
    description: node.description || '',
    length:      length,
    x:           node.x || 300,
    y:           node.y || 200,
  };
}

async function kfxCreateWire(realPid, wire) {
  return await kfxFetch('/projects/' + realPid + '/wires', {
    method: 'POST',
    body: kfxWireToBackend(wire),
  });
}

async function kfxUpdateWire(wid, patch) {
  return await kfxFetch('/wires/' + wid, {
    method: 'PUT',
    body: kfxWireToBackend(patch),
  });
}

async function kfxDeleteWire(wid) {
  return await kfxFetch('/wires/' + wid, { method: 'DELETE' });
}

async function kfxCreateConnector(realPid, conn) {
  return await kfxFetch('/projects/' + realPid + '/connectors', {
    method: 'POST',
    body: kfxConnectorToBackend(conn),
  });
}

async function kfxUpdateConnector(cid, patch) {
  return await kfxFetch('/connectors/' + cid, {
    method: 'PUT',
    body: kfxConnectorToBackend(patch),
  });
}

async function kfxDeleteConnector(cid) {
  return await kfxFetch('/connectors/' + cid, { method: 'DELETE' });
}

async function kfxCreateNode(realPid, node) {
  return await kfxFetch('/projects/' + realPid + '/nodes', {
    method: 'POST',
    body: kfxNodeToBackend(node),
  });
}

async function kfxUpdateNode(nid, patch) {
  return await kfxFetch('/nodes/' + nid, {
    method: 'PUT',
    body: kfxNodeToBackend(patch),
  });
}

async function kfxDeleteNode(nid) {
  return await kfxFetch('/nodes/' + nid, { method: 'DELETE' });
}

async function kfxCreateProject(payload) {
  return await kfxFetch('/projects', {
    method: 'POST',
    body: {
      name:         payload.name || 'New Harness',
      harness_name: payload.harness_name || payload.name || '',
      description:  payload.description || '',
    },
  });
}

async function kfxImportCsv(realPid, file) {
  const fd = new FormData();
  fd.append('file', file);
  return await kfxFetch('/projects/' + realPid + '/import-csv', {
    method: 'POST',
    body: fd,
  });
}

/* ── Export helpers ──
   Backend exports return binary content with Content-Disposition: attachment.
   For GET endpoints, build a URL with ?token=… (backend accepts query-param
   auth — see _get_session in backend/main.py) and trigger a download via
   window.location.assign. For POST endpoints (export-formboard /
   export-combined), submit a hidden form so the response becomes a download. */

function kfxExportUrl(realPid, name) {
  return KFX_API + '/projects/' + realPid + '/export-' + name +
         '?token=' + encodeURIComponent(kfxGetToken());
}

function kfxDownload(realPid, name) {
  // POST-only export endpoints. Submit a hidden form so the browser handles
  // the binary response as a file download.
  if (name === 'combined' || name === 'formboard') {
    const form = document.createElement('form');
    form.method = 'POST';
    form.action = KFX_API + '/projects/' + realPid + '/export-' + name +
                  '?token=' + encodeURIComponent(kfxGetToken());
    form.style.display = 'none';
    document.body.appendChild(form);
    form.submit();
    setTimeout(() => form.remove(), 1000);
    return;
  }
  // GET endpoints — simplest possible trigger.
  const a = document.createElement('a');
  a.href = kfxExportUrl(realPid, name);
  a.rel = 'noopener';
  document.body.appendChild(a);
  a.click();
  setTimeout(() => a.remove(), 1000);
}

/* ── Admin helpers — Backups / Users / Companies / Share-links / Health ──
   Thin wrappers over the /api/admin/* + /api/users + /api/companies
   endpoints. Each returns the raw backend payload; the admin panels are
   responsible for mapping/rendering. After mutations, callers should re-
   fetch the matching list. */

/* Backups */
async function kfxListBackups() {
  return await kfxFetch('/admin/backups');
}
async function kfxTriggerBackup() {
  return await kfxFetch('/admin/backup', { method: 'POST' });
}
async function kfxRestoreBackup(filename) {
  return await kfxFetch('/admin/backup/' + encodeURIComponent(filename) + '/restore', { method: 'POST' });
}
async function kfxDeleteBackupFile(filename) {
  return await kfxFetch('/admin/backup/' + encodeURIComponent(filename), { method: 'DELETE' });
}
async function kfxImportLegacyDb(file) {
  const fd = new FormData();
  fd.append('file', file);
  return await kfxFetch('/admin/backup/import', { method: 'POST', body: fd });
}
async function kfxGetBackupSettings() {
  // Backend returns {enabled: bool}. Normalise to a shape with
  // backups_enabled so the UI doesn't care about wire format.
  const r = await kfxFetch('/admin/backup-settings');
  return { backups_enabled: !!r.enabled };
}
async function kfxSetBackupSettings({ backups_enabled }) {
  const r = await kfxFetch('/admin/backup-settings', {
    method: 'POST',
    body: { enabled: !!backups_enabled },
  });
  return { backups_enabled: !!r.enabled };
}

/* Users */
async function kfxListUsers() {
  return await kfxFetch('/users');
}
async function kfxCreateUser(user) {
  return await kfxFetch('/users', { method: 'POST', body: user });
}
async function kfxUpdateUser(uid, patch) {
  return await kfxFetch('/users/' + uid, { method: 'PUT', body: patch });
}
async function kfxDeleteUser(uid) {
  return await kfxFetch('/users/' + uid, { method: 'DELETE' });
}

/* Companies — superadmin view uses /admin/companies (status-aware);
   POST create uses /companies (no admin/companies POST exists). */
async function kfxListCompanies() {
  return await kfxFetch('/admin/companies');
}
async function kfxCreateCompany(co) {
  return await kfxFetch('/companies', { method: 'POST', body: co });
}
async function kfxUpdateCompany(cid, patch) {
  return await kfxFetch('/companies/' + cid, { method: 'PUT', body: patch });
}
async function kfxApproveCompany(cid, trial_days = 14) {
  return await kfxFetch('/admin/companies/' + cid + '/approve', {
    method: 'POST', body: { trial_days },
  });
}
async function kfxSetCompanyStatus(cid, status) {
  return await kfxFetch('/admin/companies/' + cid + '/status', {
    method: 'POST', body: { status },
  });
}

/* Share links — keyed by the project's real (backend) id. */
async function kfxListShareLinks(realPid) {
  return await kfxFetch('/projects/' + realPid + '/shares');
}
async function kfxCreateShareLink(realPid, { expires_at, note }) {
  // Backend takes expires_in_days, not an absolute timestamp. Convert
  // here so the panel can use a date input naturally.
  let expires_in_days = null;
  if (expires_at) {
    const target = Date.parse(expires_at);
    if (!isNaN(target)) {
      const diffMs = target - Date.now();
      expires_in_days = Math.max(1, Math.ceil(diffMs / 86400000));
    }
  }
  return await kfxFetch('/projects/' + realPid + '/share', {
    method: 'POST',
    body: { expires_in_days, note: note || '' },
  });
}
async function kfxRevokeShareLink(token) {
  return await kfxFetch('/share/' + encodeURIComponent(token), { method: 'DELETE' });
}

/* Health */
async function kfxGetVersion() {
  return await kfxFetch('/version');
}
async function kfxGetHealth() {
  return await kfxFetch('/health');
}

async function kfxBootstrap() {
  if (!kfxGetToken()) return { kind: 'guest' };
  try {
    const me = await kfxLoadAuthMe();
    if (!me) { kfxClearAuth(); return { kind: 'guest' }; }
    const projects = await kfxLoadProjects();
    return { kind: 'authed', me, projects };
  } catch (e) {
    if (e.status === 401) { kfxClearAuth(); return { kind: 'guest' }; }
    return { kind: 'error', err: e };
  }
}

Object.assign(window, {
  PROJECTS, COLOR_HEX, COLOR_CODE,
  kfxGetToken, kfxSetAuth, kfxClearAuth, kfxFetch,
  kfxMapProject, kfxRelativeTime,
  kfxLoadAuthMe, kfxLoadProjects, kfxLogin, kfxLogout, kfxBootstrap,
  kfxLoadConnectorLibrary, kfxMapLibraryRow,
  kfxLoadProject, kfxLoadAndMapProject,
  kfxMapWire, kfxMapConnector, kfxMapNode, kfxMapBundles,
  kfxWireToBackend, kfxConnectorToBackend, kfxNodeToBackend,
  kfxCreateWire, kfxUpdateWire, kfxDeleteWire,
  kfxCreateConnector, kfxUpdateConnector, kfxDeleteConnector,
  kfxCreateNode, kfxUpdateNode, kfxDeleteNode,
  kfxCreateProject, kfxImportCsv,
  kfxExportUrl, kfxDownload,
  /* Admin */
  kfxListBackups, kfxTriggerBackup, kfxRestoreBackup, kfxDeleteBackupFile,
  kfxImportLegacyDb, kfxGetBackupSettings, kfxSetBackupSettings,
  kfxListUsers, kfxCreateUser, kfxUpdateUser, kfxDeleteUser,
  kfxListCompanies, kfxCreateCompany, kfxUpdateCompany, kfxApproveCompany, kfxSetCompanyStatus,
  kfxListShareLinks, kfxCreateShareLink, kfxRevokeShareLink,
  kfxGetVersion, kfxGetHealth,
});

