// Platform-agnostic Biatec BE client. Takes getToken (async fn returning
// "Bearer <jwt>" | "Token <code>" | null) and fetchImpl. Used by both
// the Chrome extension's service worker and the Electron main process.
(function (root) {
  function BiatecClient(opts) {
    this.apiBase = opts.apiBase;
    this.getToken = opts.getToken;
    // Req-F-003: optional async () => bool. On a 401 for a Bearer (admin) call,
    // it refreshes the stored access token; returning true triggers one retry.
    this.refreshAuth = opts.refreshAuth || null;
    this.fetchImpl = opts.fetch || (typeof fetch !== 'undefined' ? fetch.bind(typeof globalThis !== 'undefined' ? globalThis : root) : null);
    if (!this.fetchImpl) throw new Error('BiatecClient needs a fetch impl');
    // Build identity stamped on every request (X-BC-Version / X-BC-Platform) so
    // the BE can record which BC build a user is on (→ prefills the ticket/UAT
    // forms). Optional: omitted by the Chrome extension, supplied by Electron.
    this.clientVersion = opts.clientVersion || null;
    this.clientPlatform = opts.clientPlatform || null;
  }

  // Slightly longer than BE's own outbound NP timeout (30s) so a slow NP
  // surfaces as the BE's error message rather than a bare client-side abort.
  BiatecClient.REQUEST_TIMEOUT_MS = 45000;

  BiatecClient.prototype._call = function (method, path, body, opts) {
    return this._callOnce(method, path, body, opts || {}, false);
  };

  BiatecClient.prototype._callOnce = async function (method, path, body, opts, retried) {
    var headers = {};
    if (body !== undefined && body !== null) headers['Content-Type'] = 'application/json';
    if (this.clientVersion) headers['X-BC-Version'] = this.clientVersion;
    if (this.clientPlatform) headers['X-BC-Platform'] = this.clientPlatform;
    var usedBearer = false;
    if (opts.auth !== false) {
      var auth = await this.getToken();
      if (auth) { headers['Authorization'] = auth; usedBearer = /^Bearer /.test(auth); }
    }
    var init = { method: method, headers: headers };
    if (body !== undefined && body !== null) init.body = JSON.stringify(body);

    // Hard timeout so a slow/hung BE (e.g. NP label render on bulk print)
    // rejects instead of leaving the caller's await — and its disabled button —
    // stuck forever. Skipped where AbortSignal.timeout is unavailable.
    if (!init.signal && typeof AbortSignal !== 'undefined' && AbortSignal.timeout) {
      init.signal = AbortSignal.timeout(BiatecClient.REQUEST_TIMEOUT_MS);
    }

    var res = await this.fetchImpl(this.apiBase + path, init);

    // Req-F-003: access token expired → refresh once and retry (admin/Bearer only).
    if (res.status === 401 && !retried && usedBearer && opts.auth !== false && this.refreshAuth) {
      var refreshed = false;
      try { refreshed = await this.refreshAuth(); } catch (e) { refreshed = false; }
      if (refreshed) return this._callOnce(method, path, body, opts, true);
    }

    if (opts.responseType === 'arrayBuffer') {
      if (!res.ok) {
        var errText = await res.text();
        try { errText = JSON.parse(errText).error; } catch (e) { /* keep raw */ }
        throw new Error(errText || 'API error ' + res.status);
      }
      return res.arrayBuffer();
    }

    var json = null;
    try { json = await res.json(); } catch (e) { /* non-JSON */ }
    if (!res.ok) throw new Error((json && json.error) || 'API error ' + res.status);
    return json;
  };

  // Auth
  BiatecClient.prototype.login = function (login, password) {
    return this._call('POST', '/auth/login', { login: login, password: password }, { auth: false });
  };

  // Projects
  BiatecClient.prototype.listProjects = function () {
    return this._call('GET', '/projects');
  };
  BiatecClient.prototype.linkShoptet = function (shoptetProjectId, eshopUrl) {
    return this._call('POST', '/projects/link-shoptet', { shoptetProjectId: shoptetProjectId, eshopUrl: eshopUrl });
  };
  BiatecClient.prototype.verifyProject = function (projectId, eshopUrl) {
    return this._call('POST', '/projects/' + projectId + '/verify', { eshopUrl: eshopUrl });
  };
  BiatecClient.prototype.resetProject = function (projectId) {
    return this._call('POST', '/projects/' + projectId + '/reset', {});
  };
  // Bod 1: persist the detected Shoptet shipping-method IDs (NP branch / address)
  // to the project so the checkout widget can read them via GET /widget-config.
  BiatecClient.prototype.saveShippingMethods = function (projectId, branchMethodId, addressMethodId) {
    return this._call('PUT', '/projects/' + projectId, {
      npBranchMethodId: branchMethodId || null,
      npAddressMethodId: addressMethodId || null
    });
  };
  // Persist the allowed delivery countries [{iso,id}] detected from the e-shop's
  // /admin/countries/ page during BC onboarding. Gates which foreign NP branches
  // the picker offers and supplies the Shoptet countryId for writing the order's
  // shipping-address country (#deliveryCountryId).
  BiatecClient.prototype.saveDeliveryCountries = function (projectId, countries) {
    return this._call('PUT', '/projects/' + projectId, {
      shoptetDeliveryCountries: Array.isArray(countries) ? countries : []
    });
  };
  // WP-5b sender-branch picker (BC top-bar modal): persist the chosen NP sender
  // DIVISION for the project's ACTIVE environment. The per-env columns share
  // nothing, so we write the suffix that matches np_mode ('production' → Prod,
  // else Test). The picker is CZ-filtered, so we pin the env's sender country to
  // the branch's country too (keeps the resolved sender country consistent with
  // the division). Pass divisionId = null/'' to clear.
  BiatecClient.prototype.saveSenderDivision = function (projectId, divisionId, mode, countryCode) {
    var S = (mode === 'production') ? 'Prod' : 'Test';
    var body = {};
    body['npSenderDivisionId' + S] = (divisionId == null || divisionId === '') ? '' : Number(divisionId);
    if (countryCode) body['npSenderCountryCode' + S] = String(countryCode).toUpperCase();
    return this._call('PUT', '/projects/' + projectId, body);
  };
  // WP-5c sender-address editor (BC top-bar modal): persist the project's
  // free-text "send from" warehouse address. SHARED across test/prod (one
  // warehouse), so a single npSenderAddress field. NP /shipments turns this into
  // sender.addressParts { street, building, city, postCode, country } + an
  // AlternativeAddress service (country comes from npSenderCountryCode). Pass an
  // empty/null address to clear it (→ NULL = back to branch mode). The optional
  // `phone` overrides the profile sender phone for the pickup.
  BiatecClient.prototype.saveSenderAddress = function (projectId, address, mode, countryCode) {
    var body = { npSenderAddress: address || {} };
    // Ensure the ACTIVE env has a sender country (addressParts.country needs it),
    // without touching the other profile. mode: 'test'|'production'.
    if (countryCode) {
      body['npSenderCountryCode' + (mode === 'production' ? 'Prod' : 'Test')] = String(countryCode).toUpperCase();
    }
    return this._call('PUT', '/projects/' + projectId, body);
  };
  BiatecClient.prototype.connectOperator = function (token, shoptetProjectId, domain) {
    return this._call('POST', '/projects/connect', { token: token, shoptetProjectId: shoptetProjectId, domain: domain }, { auth: false });
  };

  // Shipments
  BiatecClient.prototype.createShipments = function (projectId, orders) {
    return this._call('POST', '/shipments', { projectId: projectId, orders: orders });
  };
  // Tic-0032: suggested parcel box (cm) for the manual-dimensions pre-fill.
  BiatecClient.prototype.suggestDimensions = function (projectId, items) {
    return this._call('POST', '/shipments/suggest-dimensions', { projectId: projectId, items: items });
  };
  BiatecClient.prototype.retryShipment = function (shipmentId) {
    return this._call('POST', '/shipments/' + shipmentId + '/retry', {});
  };
  BiatecClient.prototype.cancelShipment = function (shipmentId) {
    return this._call('POST', '/shipments/' + shipmentId + '/cancel', {});
  };
  BiatecClient.prototype.bulkRefreshShipments = function (projectId) {
    return this._call('POST', '/shipments/bulk-refresh', { projectId: projectId });
  };
  // Refresh one shipment's status live from Nova Post. Returns
  // { shipment, tracking, status } where tracking.history_tracking[] is the
  // full dated status timeline (used by the order-detail Shipments tab).
  BiatecClient.prototype.refreshShipment = function (shipmentId) {
    return this._call('POST', '/shipments/' + shipmentId + '/refresh', {});
  };
  BiatecClient.prototype.listShipments = function (projectId) {
    return this._call('GET', '/shipments?projectId=' + encodeURIComponent(projectId));
  };
  // Req-F-041 status write-back: shipments whose NP status changed and map to a
  // Shoptet order status (leased to this BC). Returns { changes: [...] }.
  BiatecClient.prototype.getPendingStatusChanges = function () {
    return this._call('GET', '/shipments/pending-status');
  };
  // Versioned ack — BC wrote `version` (np_status_at) into Shoptet. Returns
  // { synced, superseded } (superseded ⇒ a newer NP update arrived; re-poll).
  BiatecClient.prototype.ackStatusSynced = function (shipmentId, version) {
    return this._call('POST', '/shipments/' + shipmentId + '/status-synced', { version: version });
  };
  BiatecClient.prototype.getLabel = function (shipmentId) {
    return this._call('GET', '/shipments/' + shipmentId + '/label', null, { responseType: 'arrayBuffer' });
  };
  // Req-F-021/F-022: customs PDFs auto-generated by NP for cross-border shipments.
  // kind = 'invoice' (Commercial Invoice, all cross-border)
  //      | 'international' (Customs Declaration, non-EU only — UA/US).
  BiatecClient.prototype.getCustomsPdf = function (shipmentId, kind) {
    return this._call('GET', '/shipments/' + shipmentId + '/customs-pdf/' + encodeURIComponent(kind), null, { responseType: 'arrayBuffer' });
  };
  // opts: { type, printSizeType }. Bulk wizard passes type='marking' +
  // printSizeType='size_100_100' for thermal labels (UAT-022 Set 1); omit for
  // the legacy single A4 sheet.
  BiatecClient.prototype.getBulkLabels = function (ids, opts) {
    opts = opts || {};
    return this._call('POST', '/shipments/labels',
      { ids: ids, type: opts.type, printSizeType: opts.printSizeType },
      { responseType: 'arrayBuffer' });
  };
  // Req-F-024: bulk customs PDFs (Commercial Invoice + Customs Declaration).
  // BE classifies per shipment: domestic skips, EU intl gets invoice only,
  // non-EU intl gets both. Returns a single merged PDF (pdf-lib).
  BiatecClient.prototype.getBulkCustomsPdf = function (ids) {
    return this._call('POST', '/shipments/customs-pdfs', { ids: ids }, { responseType: 'arrayBuffer' });
  };

  // Req-F-026: NP branch / postomat / pickup-point search. opts may include
  // countryCodes (array or string), divisionCategories, search, cityName, limit.
  BiatecClient.prototype.divisions = function (projectId, opts) {
    opts = opts || {};
    var qs = ['projectId=' + encodeURIComponent(projectId)];
    function appendArr(name, val) {
      if (val == null) return;
      var arr = Array.isArray(val) ? val : [val];
      for (var i = 0; i < arr.length; i++) {
        if (arr[i]) qs.push(encodeURIComponent(name) + '=' + encodeURIComponent(arr[i]));
      }
    }
    appendArr('countryCodes[]', opts.countryCodes);
    appendArr('divisionCategories[]', opts.divisionCategories);
    // textSearch = NP free-text filter (branch name/number + city, Cyr+Lat).
    if (opts.textSearch) qs.push('textSearch=' + encodeURIComponent(opts.textSearch));
    if (opts.search) qs.push('search=' + encodeURIComponent(opts.search));
    if (opts.cityName) qs.push('cityName=' + encodeURIComponent(opts.cityName));
    if (opts.limit) qs.push('limit=' + encodeURIComponent(opts.limit));
    // all=1 → server pulls every page (fetch-all fallback).
    if (opts.all) qs.push('all=1');
    return this._call('GET', '/divisions?' + qs.join('&'));
  };

  // Resolve a single NP division's destination country (ISO alpha-2). Used by
  // the bulk wizard so branch deliveries classify by the branch's real country
  // (e.g. a UA pickup point) instead of the order's shipping-address country.
  // Returns { divisionId, countryCode } (countryCode null if NP has no value).
  BiatecClient.prototype.divisionCountry = function (projectId, divisionId) {
    return this._call('GET', '/divisions/country?projectId='
      + encodeURIComponent(projectId) + '&divisionId=' + encodeURIComponent(divisionId));
  };

  // Resolve a single NP division's display info (name + formatted address).
  // Used by the BC top bar to show the sender branch as "Branch number + Address"
  // instead of the bare division id. Returns { id, name, address, cityName,
  // countryCode } (fields empty/null when NP has no value).
  BiatecClient.prototype.divisionInfo = function (projectId, divisionId) {
    return this._call('GET', '/divisions/info?projectId='
      + encodeURIComponent(projectId) + '&divisionId=' + encodeURIComponent(divisionId));
  };

  // Req-F-038: B2B recipient cache lookup (auto-fills shipment-create flow
  // when the same company TIN ships repeatedly). Returns the cached row or
  // throws a 404 (caller should swallow — cache miss is normal).
  BiatecClient.prototype.getCachedB2bRecipient = function (projectId, companyTin) {
    return this._call(
      'GET',
      '/b2b-recipients/' + encodeURIComponent(projectId) + '/' + encodeURIComponent(companyTin)
    );
  };
  BiatecClient.prototype.listB2bRecipients = function (projectId, limit) {
    var url = '/b2b-recipients/' + encodeURIComponent(projectId);
    if (limit) url += '?limit=' + encodeURIComponent(limit);
    return this._call('GET', url);
  };
  // Req-F-038 / UAT-075: upsert a B2B cache row (operator curation). Used by the
  // bulk wizard to persist a confirmed/overridden EORI for an IČO so the next
  // shipment to that company pre-fills it. body = { projectId, companyTin,
  // eoriCode, companyName?, ... }.
  BiatecClient.prototype.saveB2bRecipient = function (projectId, body) {
    return this._call('POST', '/b2b-recipients',
      Object.assign({ projectId: projectId }, body || {}));
  };

  // Per-variant customs data (HS codes etc. used for cross-border shipments).
  BiatecClient.prototype.listProductCustoms = function (projectId) {
    return this._call('GET', '/products-customs?projectId=' + encodeURIComponent(projectId));
  };
  BiatecClient.prototype.productCustomsCoverage = function (projectId) {
    return this._call('GET', '/products-customs/coverage?projectId=' + encodeURIComponent(projectId));
  };
  BiatecClient.prototype.importProductCustoms = function (projectId, rows, matchBy) {
    return this._call('POST', '/products-customs/import', {
      projectId: projectId,
      rows: rows,
      // 'variant_guid' (default) | 'product_code' — chosen by the operator at import.
      matchBy: matchBy === 'product_code' ? 'product_code' : 'variant_guid'
    });
  };
  BiatecClient.prototype.seedProductCustoms = function (projectId, rows) {
    return this._call('POST', '/products-customs/seed', { projectId: projectId, rows: rows });
  };
  BiatecClient.prototype.updateProductCustoms = function (projectId, variantGuid, patch) {
    return this._call(
      'PATCH',
      '/products-customs/' + encodeURIComponent(variantGuid) + '?projectId=' + encodeURIComponent(projectId),
      patch
    );
  };
  // Upsert by variantGuid OR productCode — used by the orders-list / overlay
  // "Fill customs data" wizard when the Shoptet order detail doesn't carry
  // variant GUIDs (e.g. sametic.cz template). BE synthesizes variant_guid
  // from productCode so subsequent cross-border shipments hit cache.
  BiatecClient.prototype.upsertProductCustoms = function (projectId, body) {
    return this._call('POST', '/products-customs/upsert',
      Object.assign({ projectId: projectId }, body || {}));
  };
  // Server-side DeepL proxy. Auto-detects source language; returns
  // [{ text, detectedSourceLang }] in the same order as the input.
  BiatecClient.prototype.translateTexts = function (texts, targetLang) {
    return this._call('POST', '/products-customs/translate', {
      texts: texts,
      targetLang: targetLang || 'EN-US'
    });
  };

  // Health check — used by top bar status indicators in Electron app.
  BiatecClient.prototype.ping = async function () {
    try {
      var res = await this.fetchImpl(this.apiBase + '/health', { method: 'GET' });
      return res.ok;
    } catch (e) {
      return false;
    }
  };

  // Upstream Nova Post reachability. BE probes NP on each call (cached 10s
  // server-side). Returns { ok, latencyMs, upstreamStatus? } or { ok: false }
  // if BE itself is unreachable.
  BiatecClient.prototype.pingNovaPost = async function () {
    try {
      var res = await this.fetchImpl(this.apiBase + '/health/novapost', { method: 'GET' });
      if (!res.ok) return { ok: false };
      return await res.json();
    } catch (e) {
      return { ok: false };
    }
  };

  var api = { BiatecClient: BiatecClient };
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
  else { root.NP = root.NP || {}; root.NP.api = api; }
})(typeof self !== 'undefined' ? self : this);
