/* global React */
// Data layer for CloudGate — every table comes from the live database via
// window.__INITIAL_DATA__ (injected server-side from OmnigateDbContext).
// No mock data: tables not in the schema (Products, Stock, Gateways, Zones)
// surface as empty arrays; pages that depend on them render empty until those
// tables are added to the database.

// CloudGate web-app version — shown under the sidebar brand for SystemAdmin
// only.  Bump on every release that ships UI / API changes worth surfacing.
const CLOUDGATE_VERSION = '1.7.76';

const NOW = new Date();

// ── DB tables — from server ────────────────────────────────────────────────
const __D = (window.__INITIAL_DATA__ && window.__INITIAL_DATA__.DB) || {};

const USER_LEVELS     = __D.UserLevels      || [];
const READER_TYPES    = __D.ReaderTypes     || [];
const READER_STATUSES = __D.ReaderStatuses  || [];
const TAG_TYPES       = __D.TagTypes        || [];
const ASSET_STATUSES  = __D.AssetStatuses   || [];
const LOCATION_TYPES  = __D.LocationTypes   || [];
const LOCATIONS       = __D.Locations       || [];
const COMPANY_TYPES   = __D.CompanyTypes    || [];
const COMPANIES       = __D.Companies       || [];
const USERS           = __D.Users           || [];
const ASSETS          = __D.Assets          || [];   // empty — see /api/assets
const ASSET_COUNTS    = __D.AssetCounts     || [];   // [{LocationId,AssetTypeId,StatusId,Count}]
const ASSET_COUNT_TOTAL = __D.AssetCountTotal ?? 0;
const READERS         = __D.Readers         || [];
const ASSET_TYPES     = __D.AssetTypes       || [];
const LOC_ASSET_QTYS  = __D.LocationAssetQuantities || [];
const TENANTS         = __D.Tenants          || [];
const CUSTOM_FIELDS   = __D.CustomFields    || [];
const COMPANY_HIDDEN_PAGES = __D.CompanyHiddenPages || [];   // [{CompanyId,PageKey}]

const FIELD_TYPE_LABELS = ['String','Integer','Double','DateTime','Bool','Image'];

// ── Toggleable main pages (the "Company Pages" editor + sidebar gating) ────
// Every page a company-scoped user (CustomerAdmin / Operator / Viewer / API)
// can ever see, in sidebar order.  All are visible by default; a row in
// CompanyHiddenPages hides one for that Company's users.  `demoOnly` pages
// only exist in the Demo deployment, so the editor lists them only there.
// `key` matches the sidebar route id AND CompanyPageKeys in CrudApi.cs.
const COMPANY_PAGES = [
  { key: 'dashboard',        label: 'Dashboard' },
  { key: 'inventory',        label: 'Inventory' },
  { key: 'locations-admin',  label: 'Locations' },
  { key: 'companies',        label: 'Company' },
  { key: 'users',            label: 'Users' },
  { key: 'stocking-targets', label: 'Stocking targets' },
  { key: 'asset-types',      label: 'Asset types' },
  { key: 'assets',           label: 'Assets' },
  { key: 'readers-admin',    label: 'Readers' },
];

// Set of page keys hidden for one Company.  Reads live from CloudGate.DB so
// it reflects edits made on the Company Pages screen without a reload.
function hiddenPagesForCompany(companyId) {
  const rows = (window.CloudGate?.DB?.CompanyHiddenPages) || [];
  return new Set(rows.filter(h => h.CompanyId === companyId).map(h => h.PageKey));
}

// Companies the current user is allowed to see anywhere — drawers, picker
// dropdowns, list views.  The server snapshot is already scoped by the
// caller's role.  On top of that, the topbar may further narrow the view
// down to a single Tenant or a single Company; that selection lives in
// `window.__activeScope` as one of:
//   { kind:'all',     id:null }   — no client-side narrowing
//   { kind:'tenant',  id:<n>  }   — only Companies whose TenantId == n
//   { kind:'company', id:<n>  }   — only the single Company with Id == n
function visibleCompanies() {
  const u   = window.__currentUser;
  const all = (window.CloudGate?.DB?.Companies) || [];
  if (!u) return all;
  const scope = window.__activeScope || { kind:'all', id:null };
  if (scope.kind === 'tenant')  return all.filter(c => c.TenantId === scope.id);
  if (scope.kind === 'company') return all.filter(c => c.Id       === scope.id);
  return all;   // 'all' — server already enforced the role's outer bound.
}

// Filter rows of any CompanyId-keyed table to those owned by a Company the
// current user can see.  Used by every list page so the same scoping rule
// applies whether you're on Locations, Users, Assets, Readers, AssetTypes
// or LocationAssetQuantities.  When the rows array isn't available yet
// (mid-render before login) we return [] so React renders an empty list
// instead of crashing.
function byActiveCompany(rows, field) {
  field = field || 'CompanyId';
  if (!Array.isArray(rows)) return rows || [];
  const ids = new Set(visibleCompanies().map(c => c.Id));
  return rows.filter(r => ids.has(r[field]));
}

// Role gate for editable rows on Locations, Companies, Users, Stocking targets.
// Only SystemAdmin / TenantAdmin / CustomerAdmin (UserLevel Id 1–3) can mutate
// those tables.  Operator / Viewer / API see read-only views (no Edit, Delete
// or New buttons; Stocking-target inputs disabled).
function canEdit() {
  const r = window.__currentUser?.role;
  return r === 'SystemAdmin' || r === 'TenantAdmin' || r === 'CustomerAdmin';
}

// Stricter gate for device hardware (Readers).  CustomerAdmin can run their
// own warehouse but is not allowed to add/edit/delete the physical reader
// inventory — only the Tenant or the System administrator manage devices.
function canEditDevices() {
  const r = window.__currentUser?.role;
  return r === 'SystemAdmin' || r === 'TenantAdmin';
}

// Operator-and-above gate — used by bulk operational features like tag import
// where Operator (read+execute) is the minimum required role.  Mirrors
// Authz.CanOperate on the server.
function canOperate() {
  const r = window.__currentUser?.role;
  return r === 'SystemAdmin' || r === 'TenantAdmin' || r === 'CustomerAdmin' || r === 'Operator';
}

// True only for SystemAdmin — the only role allowed to see internal Id values
// in admin lists.
function isAdmin() { return window.__currentUser?.role === 'SystemAdmin'; }

// ── Deployment mode (Production | Demo | Test) ─────────────────────────────
// Injected by the server into window.__APP_MODE__ (CloudGate.html + Program.cs).
//  • Demo  — marketing flourishes (login stats, dashboard metric cards) + a
//            DEMO badge.
//  • Production / Test — those flourishes stripped; Inventory/Stocking hidden
//            from the menu.  (Test == Production UI, different DB.)
const APP_MODE = (window.__APP_MODE__ === 'Demo' || window.__APP_MODE__ === 'Test')
                   ? window.__APP_MODE__ : 'Production';
function isDemo()       { return APP_MODE === 'Demo'; }
function isProduction() { return APP_MODE === 'Production'; }
function isTest()       { return APP_MODE === 'Test'; }
// "Stripped" UI = everything except Demo.
function showMarketingUi() { return APP_MODE === 'Demo'; }

// Roles allowed to see the Company column on list pages AND to create a new
// Company.  Tenant/System administrators only — a "customer" (CustomerAdmin)
// manages a single company and may not create others.
function isTenantOrSystemAdmin() {
  const r = window.__currentUser?.role;
  return r === 'SystemAdmin' || r === 'TenantAdmin';
}
function canCreateCompany() { return isTenantOrSystemAdmin(); }

function makeEpc() {
  const hex = () => Math.floor(Math.random() * 256).toString(16).padStart(2, '0').toUpperCase();
  return Array.from({length: 12}, hex).join('');
}
function dashEpc(epc) { return epc.match(/.{2}/g).join('-'); }
function fmtDate(iso) {
  if (!iso) return '—';
  const d = new Date(iso);
  return d.toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' });
}
function relTime(iso) {
  if (!iso) return '—';
  const diff = (NOW - new Date(iso)) / 60000;
  if (diff < 1) return 'just now';
  if (diff < 60) return Math.round(diff) + 'm ago';
  if (diff < 1440) return Math.round(diff/60) + 'h ago';
  return Math.round(diff/1440) + 'd ago';
}

// ── Generic CRUD helpers ──────────────────────────────────────────────────
// `entity` is the URL segment (e.g. 'locations', 'companies').
// On create, the row's Id is missing → POST returns the saved row with a real Id.
// On update, row.Id is set → PUT returns the saved row.
async function apiSave(entity, row) {
  const isUpdate = !!row.Id;
  const url      = '/api/' + entity + (isUpdate ? '/' + row.Id : '');
  const method   = isUpdate ? 'PUT' : 'POST';
  try {
    const res = await fetch(url, {
      method,
      headers: { 'Content-Type': 'application/json' },
      body:    JSON.stringify(row)
    });
    if (!res.ok) {
      const text = await res.text();
      console.warn('[CRUD] ' + method + ' ' + url + ' failed', res.status, text);
      alert('Save failed: ' + text);
      return null;
    }
    return await res.json();
  } catch (err) {
    console.warn('[CRUD] ' + method + ' ' + url + ' error', err);
    return null;
  }
}
// ── Stocking-target upsert ──────────────────────────────────────────────────
// Server endpoint upserts by (LocationId, AssetTypeId): zero/zero/zero → delete.
async function apiUpsertQty(locationId, assetTypeId, target, minimum, maximum) {
  try {
    const res = await fetch('/api/locationassetquantities', {
      method:  'POST',
      headers: { 'Content-Type': 'application/json' },
      body:    JSON.stringify({ LocationId: locationId, AssetTypeId: assetTypeId,
                                Target: +target || 0, Minimum: +minimum || 0, Maximum: +maximum || 0 })
    });
    if (!res.ok) { console.warn('[QTY] upsert failed', res.status); return null; }
    return await res.json();
  } catch (err) { console.warn('[QTY] upsert error', err); return null; }
}

// ── Asset count helpers (work over the AssetCounts aggregate) ─────────────
// AssetCounts ships from /api/snapshot as one row per (Location, AssetType,
// Status) cell with its Count.  All four helpers below are pure roll-ups over
// that array — cheap even with thousands of cells, and zero allocations for
// the actual Asset rows (those live behind GET /api/assets).
//
// Each helper accepts optional filters; nulls mean "any".  `activeOnly` is
// a convenience for the common "only Status=Active" filter.
function _assetCountsSum({ locationId = null, assetTypeId = null, statusId = null } = {}) {
  const counts = (window.CloudGate?.DB?.AssetCounts) || [];
  let sum = 0;
  for (const c of counts) {
    if (locationId  != null && c.LocationId  !== locationId)  continue;
    if (assetTypeId != null && c.AssetTypeId !== assetTypeId) continue;
    if (statusId    != null && c.StatusId    !== statusId)    continue;
    sum += c.Count;
  }
  return sum;
}
function assetCountAt(locationId, assetTypeId, statusId)
  { return _assetCountsSum({ locationId, assetTypeId, statusId }); }
function assetCountByLocation(locationId,  statusId)
  { return _assetCountsSum({ locationId, statusId }); }
function assetCountByAssetType(assetTypeId, statusId)
  { return _assetCountsSum({ assetTypeId, statusId }); }
function assetCountByStatus(statusId)
  { return _assetCountsSum({ statusId }); }
function assetCountTotal()
  { return _assetCountsSum({}); }

// ── Paginated Assets fetch (GET /api/assets) ──────────────────────────────
async function fetchAssets({ page = 1, size = 100, q = '',
                             tagTypeId = null, statusId = null,
                             locationId = null, assetTypeId = null,
                             signal } = {}) {
  const params = new URLSearchParams();
  params.set('page', String(page));
  params.set('size', String(size));
  if (q)                      params.set('q',           q);
  if (tagTypeId   != null)    params.set('tagTypeId',   String(tagTypeId));
  if (statusId    != null)    params.set('statusId',    String(statusId));
  if (locationId  != null)    params.set('locationId',  String(locationId));
  if (assetTypeId != null)    params.set('assetTypeId', String(assetTypeId));
  try {
    const res = await fetch('/api/assets?' + params.toString(), { signal });
    if (!res.ok) {
      console.warn('[Assets] fetch failed', res.status);
      return { Total: 0, Page: page, Size: size, Rows: [] };
    }
    return await res.json();
  } catch (err) {
    if (err.name !== 'AbortError') console.warn('[Assets] fetch error', err);
    return { Total: 0, Page: page, Size: size, Rows: [] };
  }
}

// ── Bullet status for one Location ─────────────────────────────────────────
// 'red'   = at least one (location, assetType) target is breached
// 'green' = at least one target row exists, all in range
// 'black' = no targets defined for this location
function locationBulletStatus(locationId) {
  const qtys = (window.CloudGate?.DB?.LocationAssetQuantities || []).filter(q => q.LocationId === locationId);
  if (qtys.length === 0) return 'black';
  const activeId = (window.CloudGate?.DB?.AssetStatuses || []).find(s => s.Status === 'Active')?.Id;
  for (const q of qtys) {
    const count = assetCountAt(locationId, q.AssetTypeId, activeId);
    if (count < q.Minimum || count > q.Maximum) return 'red';
  }
  return 'green';
}

// Drain UDF values entered for a brand-new (just-created) row.  The
// CustomFieldsSection mirrored them onto window.__udfPendingNew[entityName]
// while the row had no Id; now that the server returned an Id we POST each
// value to /api/udf/value.  Returns the map { fieldDefId → raw } of values
// that were successfully written, so the caller can patch them into the
// saved row's in-memory FieldValues without a round-trip.
async function flushNewUdfs(entityName, newId) {
  const pending = window.__udfPendingNew?.[entityName];
  if (!pending) return null;
  delete window.__udfPendingNew[entityName];
  const written = {};
  for (const [defIdStr, raw] of Object.entries(pending)) {
    if (raw === '' || raw == null) continue;
    try {
      const res = await fetch('/api/udf/value', {
        method:  'POST',
        headers: { 'Content-Type': 'application/json' },
        body:    JSON.stringify({ FieldDefinitionId: +defIdStr, EntityId: newId, Value: String(raw) })
      });
      if (res.ok) written[defIdStr] = raw;
      else console.warn('[UDF] new-row save failed', res.status, await res.text().catch(()=>''));
    } catch (err) { console.warn('[UDF] new-row save error', err); }
  }
  return written;
}

async function apiDelete(entity, id) {
  const url = '/api/' + entity + '/' + id;
  try {
    const res = await fetch(url, { method: 'DELETE' });
    if (!res.ok && res.status !== 404) {
      console.warn('[CRUD] DELETE ' + url + ' failed', res.status);
      alert('Delete failed');
      return false;
    }
    return true;
  } catch (err) {
    console.warn('[CRUD] DELETE ' + url + ' error', err);
    return false;
  }
}

window.CloudGate = {
  DB: {
    LocationTypes: LOCATION_TYPES, Locations: LOCATIONS,
    CompanyTypes: COMPANY_TYPES, Companies: COMPANIES,
    Users: USERS, UserLevels: USER_LEVELS,
    Assets: ASSETS,                              // empty in snapshot — see fetchAssets()
    AssetCounts: ASSET_COUNTS,                   // aggregate roll-ups
    AssetCountTotal: ASSET_COUNT_TOTAL,
    TagTypes: TAG_TYPES, AssetStatuses: ASSET_STATUSES,
    Readers: READERS, ReaderTypes: READER_TYPES, ReaderStatuses: READER_STATUSES,
    AssetTypes: ASSET_TYPES,
    LocationAssetQuantities: LOC_ASSET_QTYS,
    Tenants: TENANTS,
    CustomFields: CUSTOM_FIELDS,
    CompanyHiddenPages: COMPANY_HIDDEN_PAGES,
  },
  FIELD_TYPE_LABELS,
  COMPANY_PAGES,
  version: CLOUDGATE_VERSION,
  makeEpc, dashEpc, fmtDate, relTime,
  apiSave, apiDelete, apiUpsertQty, flushNewUdfs, locationBulletStatus,
  fetchAssets,
  assetCountAt, assetCountByLocation, assetCountByAssetType, assetCountByStatus, assetCountTotal,
  byActiveCompany, visibleCompanies, canEdit, canEditDevices, canOperate, isAdmin,
  hiddenPagesForCompany,
  // Deployment mode + extra role gates.
  APP_MODE, isDemo, isProduction, isTest, showMarketingUi,
  isTenantOrSystemAdmin, canCreateCompany,
};
