// api.jsx — все API методы, JWT auto-refresh, window.api
(() => {
  const BASE = '';
  const LS = { access: 'crm_access', refresh: 'crm_refresh', user: 'crm_user' };
  let _refreshPromise = null;

  async function req(method, path, body, retry = true) {
    const token = localStorage.getItem(LS.access);
    const opts = {
      method,
      headers: {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
      },
      ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
    };
    let res;
    try {
      res = await fetch(BASE + path, opts);
    } catch (err) {
      throw new Error('Нет соединения с сервером');
    }

    if (res.status === 401 && retry) {
      if (!_refreshPromise) {
        _refreshPromise = (async () => {
          const rToken = localStorage.getItem(LS.refresh);
          if (!rToken) { logout(); throw new Error('No refresh token'); }
          const r = await fetch(BASE + '/api/auth/refresh', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ refresh: rToken }),
          });
          if (!r.ok) { logout(); throw new Error('Refresh failed'); }
          const d = await r.json();
          localStorage.setItem(LS.access, d.access);
          _refreshPromise = null;
        })();
      }
      try {
        await _refreshPromise;
      } catch (e) {
        _refreshPromise = null;
        throw e;
      }
      return req(method, path, body, false);
    }

    if (!res.ok) {
      let err = {};
      try { err = await res.json(); } catch {}
      const e = new Error(err.error || err.message || `HTTP ${res.status}`);
      e.status = res.status;
      // Прокидываем доп. поля (например, soft+conflicts для 409 расписания),
      // чтобы вызывающий код мог различить «жёсткую» ошибку от «мягкой» с
      // возможностью повтора.
      for (const k of Object.keys(err)) {
        if (k !== 'error' && k !== 'message') e[k] = err[k];
      }
      throw e;
    }
    if (res.status === 204) return null;
    return res.json();
  }

  function get(path) { return req('GET', path); }
  function post(path, body) { return req('POST', path, body); }
  function put(path, body) { return req('PUT', path, body); }
  function patch(path, body) { return req('PATCH', path, body); }
  function del(path) { return req('DELETE', path); }

  // Загрузка бинарного файла с авторизацией. Имя берётся из Content-Disposition,
  // если сервер отдал, иначе используется fallbackName.
  async function download(path, fallbackName) {
    const token = localStorage.getItem(LS.access);
    const res = await fetch(BASE + path, {
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    });
    if (!res.ok) {
      let msg = `HTTP ${res.status}`;
      try { const j = await res.json(); msg = j.error || j.message || msg; } catch {}
      throw new Error(msg);
    }
    let filename = fallbackName;
    const cd = res.headers.get('Content-Disposition') || '';
    const m = cd.match(/filename\*=UTF-8''([^;]+)/i) || cd.match(/filename="?([^";]+)"?/i);
    if (m) filename = decodeURIComponent(m[1]);
    const blob = await res.blob();
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = filename; document.body.appendChild(a);
    a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  function buildQs(p) {
    if (!p) return '';
    const q = new URLSearchParams();
    Object.entries(p).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') q.set(k, v); });
    const s = q.toString();
    return s ? '?' + s : '';
  }

  function logout() {
    // Best-effort: говорим бэкенду удалить refresh-токены этого юзера, чтобы
    // ранее украденные refresh-jti перестали работать. Не ждём ответа — даже
    // если бэкенд недоступен, локально всё равно стираем хранилище.
    const token = localStorage.getItem(LS.access);
    if (token) {
      try {
        fetch('/api/auth/logout', {
          method: 'POST',
          headers: { Authorization: `Bearer ${token}` },
          keepalive: true,
        }).catch(() => {});
      } catch {}
    }
    localStorage.removeItem(LS.access);
    localStorage.removeItem(LS.refresh);
    localStorage.removeItem(LS.user);
    window.location.reload();
  }

  window.api = {
    logout,
    auth: {
      login: (d) => post('/api/auth/login', d),
      me: () => get('/api/auth/me'),
      // Публичный список пользователей для выпадашки на экране входа (без токена).
      loginUsers: () => get('/api/auth/login-users'),
    },
    clients: {
      list: (p) => get('/api/clients' + buildQs(p)),
      search: (q) => get('/api/clients/search?q=' + encodeURIComponent(q)),
      birthdaysToday: () => get('/api/clients/birthdays-today'),
      get: (id) => get('/api/clients/' + id),
      create: (d) => post('/api/clients', d),
      update: (id, d) => patch('/api/clients/' + id, d),
      delete: (id) => del('/api/clients/' + id),
      uploadPhoto: (id, blob) => {
        const token = localStorage.getItem(LS.access);
        const fd = new FormData();
        fd.append('photo', blob, 'avatar.jpg');
        return fetch(`/api/clients/${id}/photo`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${token}` },
          body: fd,
        }).then(async r => {
          if (!r.ok) { let err = {}; try { err = await r.json(); } catch {}
            throw new Error(err.error || `HTTP ${r.status}`); }
          return r.json();
        });
      },
      deletePhoto: (id) => del(`/api/clients/${id}/photo`),
      deleteCheck: (id) => get('/api/clients/' + id + '/delete-check'),
      merge: (keep_id, merge_id) => post('/api/clients/merge', { keep_id, merge_id }),
      restore: (id) => patch('/api/clients/' + id, { active: true }),
      bulkStatus: (ids, status_id) => post('/api/clients/bulk-status', { ids, status_id }),
      bulkDelete: (ids) => post('/api/clients/bulk-delete', { ids }),
      bulkRestore: (ids) => post('/api/clients/bulk-restore', { ids }),
      bulkPurge: (ids) => post('/api/clients/bulk-purge', { ids }),
      purge: (id) => post(`/api/clients/${id}/purge`),
      addComment: (id, d) => post(`/api/clients/${id}/comments`, d),
      deleteComment: (id, cid) => del(`/api/clients/${id}/comments/${cid}`),
      setTags: (id, tag_ids) => post(`/api/clients/${id}/tags`, { tag_ids }),
      bulkTags: (ids, tag_id, action) => post('/api/clients/bulk-tags', { ids, tag_id, action }),
      getMedLeaves: (id) => get(`/api/clients/${id}/medical-leaves`),
      addMedLeave: (id, d) => post(`/api/clients/${id}/medical-leaves`, d),
      deleteMedLeave: (id, lid) => del(`/api/clients/${id}/medical-leaves/${lid}`),
      addRelative: (id, relative_id, relation_type) => post(`/api/clients/${id}/relatives`, { relative_id, relation_type }),
      removeRelative: (id, rid, type) => del(`/api/clients/${id}/relatives/${rid}?type=${encodeURIComponent(type)}`),
      lkSetPassword: (id, password) => post(`/api/clients/${id}/lk-set-password`, { password }),
      lkClearPassword: (id) => post(`/api/clients/${id}/lk-clear-password`, {}),
      lkMarkVerified: (id) => post(`/api/clients/${id}/lk-mark-verified`, {}),
      lkSendReset: (id) => post(`/api/clients/${id}/lk-send-reset`, {}),
    },
    subscriptions: {
      list: (p) => get('/api/subscriptions' + buildQs(p)),
      get: (id) => get('/api/subscriptions/' + id),
      create: (d) => post('/api/subscriptions', d),
      update: (id, d) => patch('/api/subscriptions/' + id, d),
      delete: (id, d) => req('DELETE', '/api/subscriptions/' + id, d || {}),
      freeze: (id, d) => post(`/api/subscriptions/${id}/freeze`, d),
      unfreeze: (id) => post(`/api/subscriptions/${id}/unfreeze`),
      assign: (id, d) => post(`/api/subscriptions/${id}/assign`, d),
      pay: (id, d) => post(`/api/subscriptions/${id}/pay`, d),
    },
    subTypes: {
      list: () => get('/api/subscription-types'),
      create: (d) => post('/api/subscription-types', d),
      update: (id, d) => patch('/api/subscription-types/' + id, d),
    },
    visits: {
      list: (p) => get('/api/visits' + buildQs(p)),
      // Привязка отметок к конкретному schedule_item — поддерживает кейс
      // «у тренера 2 разных занятия в день, клиент посещает оба».
      bySchedule: (sid) => get(`/api/visits/by-schedule/${sid}`),
      historyBySchedule: (sid, days = 60) => get(`/api/visits/history-by-schedule/${sid}?days=${days}`),
      // По тренеру — для отметки вне расписания / служебных вызовов.
      byTeacher: (tid, date) => get(`/api/visits/by-teacher/${tid}?date=${date}`),
      historyByTeacher: (tid, date, days = 60) => get(`/api/visits/history-by-teacher/${tid}?date=${date}&days=${days}`),
      mark: (d) => post('/api/visits', d),
      bulk: (d) => post('/api/visits/bulk', d),
      delete: (id) => del('/api/visits/' + id),
    },
    schedule: {
      list: (p) => get('/api/schedule' + buildQs(p)),
      today: () => get('/api/schedule/today'),
      create: (d) => post('/api/schedule', d),
      update: (id, d) => patch('/api/schedule/' + id, d),
      invoiceLink: (id) => get('/api/schedule/' + id + '/invoice-link'),
      delete: (id) => del('/api/schedule/' + id),
      generate: (d) => post('/api/schedule/generate', d),
      extend: (d) => post('/api/schedule/extend', d),
    },
    groups: {
      list: (p) => get('/api/groups' + buildQs(p)),
      get: (id) => get('/api/groups/' + id),
      create: (d) => post('/api/groups', d),
      update: (id, d) => patch('/api/groups/' + id, d),
      templates: (gid) => get(`/api/groups/${gid}/schedule-templates`),
      addTemplate: (gid, d) => post(`/api/groups/${gid}/schedule-templates`, d),
      deleteTemplate: (gid, tid) => del(`/api/groups/${gid}/schedule-templates/${tid}`),
    },
    teachers: {
      list: (p) => get('/api/teachers' + buildQs(p)),
      get: (id) => get('/api/teachers/' + id),
      create: (d) => post('/api/teachers', d),
      update: (id, d) => patch('/api/teachers/' + id, d),
      delete: (id) => del('/api/teachers/' + id),
      // Загрузка кропнутого фото (Blob из canvas.toBlob). Возвращает {photo_url, teacher}.
      uploadPhoto: (id, blob) => {
        const token = localStorage.getItem(LS.access);
        const fd = new FormData();
        fd.append('photo', blob, 'avatar.jpg');
        return fetch(`/api/teachers/${id}/photo`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${token}` },
          body: fd,
        }).then(async r => {
          if (!r.ok) {
            let err = {}; try { err = await r.json(); } catch {}
            throw new Error(err.error || `HTTP ${r.status}`);
          }
          return r.json();
        });
      },
      deletePhoto: (id) => del(`/api/teachers/${id}/photo`),
    },
    users: {
      list: () => get('/api/users'),
      create: (d) => post('/api/users', d),
      update: (id, d) => patch('/api/users/' + id, d),
      permissionsCatalog: () => get('/api/users/permissions-catalog'),
      uploadPhoto: (id, blob) => {
        const token = localStorage.getItem(LS.access);
        const fd = new FormData();
        fd.append('photo', blob, 'avatar.jpg');
        return fetch(`/api/users/${id}/photo`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${token}` },
          body: fd,
        }).then(async r => {
          if (!r.ok) { let err = {}; try { err = await r.json(); } catch {}
            throw new Error(err.error || `HTTP ${r.status}`); }
          return r.json();
        });
      },
      deletePhoto: (id) => del(`/api/users/${id}/photo`),
    },
    friends: {
      list: (p) => get('/api/friends' + buildQs(p)),
      create: (d) => post('/api/friends', d),
      update: (id, d) => patch('/api/friends/' + id, d),
      delete: (id) => del('/api/friends/' + id),
      uploadPhoto: (id, blob) => {
        const token = localStorage.getItem(LS.access);
        const fd = new FormData();
        fd.append('photo', blob, 'avatar.jpg');
        return fetch(`/api/friends/${id}/photo`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${token}` },
          body: fd,
        }).then(async r => {
          if (!r.ok) { let err = {}; try { err = await r.json(); } catch {}
            throw new Error(err.error || `HTTP ${r.status}`); }
          return r.json();
        });
      },
      deletePhoto: (id) => del(`/api/friends/${id}/photo`),
      channels: (friendId) => get(`/api/messages/friend-channels/${friendId}`),
      sendEmail: (friendId, text, subject) =>
        post('/api/messages/email-to-friend',
          { friend_id: friendId, text, ...(subject ? { subject } : {}) }),
      messages: (friendId, limit) =>
        get(`/api/messages/by-friend/${friendId}` + (limit ? `?limit=${limit}` : '')),
    },
    birthdays: {
      list: (p) => get('/api/birthdays' + buildQs(p)),
    },
    salary: {
      teacher: (id, p) => get(`/api/salary/teacher/${id}` + buildQs(p)),
      approve: (d) => post('/api/salary/approve', d),
      paid: (id) => post(`/api/salary/${id}/paid`),
      periods: (p) => get('/api/salary/periods' + buildQs(p)),
    },
    reports: {
      revenue: (p) => get('/api/reports/revenue' + buildQs(p)),
      attendance: (p) => get('/api/reports/attendance' + buildQs(p)),
      debts: () => get('/api/reports/debts'),
      unpaidSubscriptions: () => get('/api/reports/unpaid-subscriptions'),
      expiring: (days = 7) => get(`/api/reports/expiring?days=${days}`),
      newClients: (p) => get('/api/reports/new-clients' + buildQs(p)),
      bySource:   (p) => get('/api/reports/by-source' + buildQs(p)),
      xlsx: (tab, p) => {
        const map = {
          revenue: 'revenue', attendance: 'attendance', debts: 'debts',
          expiring: 'expiring', newClients: 'new-clients', bySource: 'by-source',
          payments: 'payments',
        };
        const ep = map[tab];
        if (!ep) return Promise.reject(new Error('Неизвестный отчёт'));
        return download(`/api/reports/${ep}/xlsx` + buildQs(p), `report_${tab}.xlsx`);
      },
    },
    transactions: {
      list: (p) => get('/api/transactions' + buildQs(p)),
      delete: (id, d) => req('DELETE', '/api/transactions/' + id, d || {}),
    },
    dashboard: {
      getLayout: () => get('/api/dashboard/layout'),
      saveLayout: (layout) => req('PUT', '/api/dashboard/layout', { layout }),
    },
    weather: {
      current: () => get('/api/weather/current'),
    },
    rental: {
      rates: (teacherId) => get('/api/rental/rates?teacher_id=' + teacherId),
      rateCreate: (d) => post('/api/rental/rates', d),
      rateUpdate: (id, d) => patch('/api/rental/rates/' + id, d),
      rateDelete: (id) => del('/api/rental/rates/' + id),
      invoicePreview: (d) => post('/api/rental/invoice/preview', d),
      invoiceIssue: (d) => post('/api/rental/invoice/issue', d),
      invoices: (teacherId) => get('/api/rental/invoices?teacher_id=' + teacherId),
      invoice: (id) => get('/api/rental/invoices/' + id),
      invoiceDelete: (id, mode) => del('/api/rental/invoices/' + id + (mode ? '?mode=' + encodeURIComponent(mode) : '')),
      invoicePay: (id, d) => post('/api/rental/invoices/' + id + '/pay', d),
      invoiceXlsx: (id) => download('/api/rental/invoices/' + id + '/xlsx', 'rental_invoice_' + id + '.xlsx'),
      invoicePdf: (id, bank) => download(
        '/api/rental/invoices/' + id + '/pdf' + (bank ? '?bank=' + bank : ''),
        'schet_arenda_' + id + '.pdf'),
      tenant: (teacherId) => get('/api/rental/tenant/' + teacherId),
      tenantSave: (teacherId, d) => put('/api/rental/tenant/' + teacherId, d),
      tenantsSummary: () => get('/api/rental/tenants/summary'),
      invoicesUnpaid: () => get('/api/rental/invoices/unpaid'),
      invoiceRecipient: (invoiceId) => get('/api/rental/invoices/' + invoiceId + '/recipient'),
      sbpLinkGet: (invoiceId) => get('/api/rental/invoices/' + invoiceId + '/sbp-link'),
      sbpLinkCreate: (invoiceId, d) => post('/api/rental/invoices/' + invoiceId + '/sbp-link', d || {}),
      sbpLinkCancel: (invoiceId) => del('/api/rental/invoices/' + invoiceId + '/sbp-link'),
      // ── Прочая аренда (без расписания) ──
      premisesList: () => get('/api/rental/premises'),
      premisesCreate: (d) => post('/api/rental/premises', d),
      premisesUpdate: (id, d) => patch('/api/rental/premises/' + id, d),
      premisesDelete: (id) => del('/api/rental/premises/' + id),
      flatLines: (teacherId, unbilled) => get(
        '/api/rental/flat-lines?teacher_id=' + teacherId + (unbilled ? '&unbilled=1' : '')),
      flatLineCreate: (d) => post('/api/rental/flat-lines', d),
      flatLineUpdate: (id, d) => patch('/api/rental/flat-lines/' + id, d),
      flatLineDelete: (id) => del('/api/rental/flat-lines/' + id),
      invoiceIssueFlat: (d) => post('/api/rental/invoice/issue-flat', d),
      invoiceIssueOneOff: (d) => post('/api/rental/invoice/issue-one-off', d),
      oneOffs: () => get('/api/rental/one-offs'),
      // Отправка счёта
      sendEmail: (id, d) => post('/api/rental/invoices/' + id + '/send-email', d || {}),
      sendSmsLink: (id, d) => post('/api/rental/invoices/' + id + '/send-sms-link', d || {}),
    },
    tenantSign: {
      list: (teacherId) => get('/api/tenant-sign?teacher_id=' + teacherId),
      detail: (id) => get('/api/tenant-sign/' + id),
      upload: (teacherId, title, file, onProgress) => {
        const fd = new FormData();
        fd.append('teacher_id', String(teacherId));
        fd.append('title', title);
        fd.append('file', file, file.name);
        if (typeof onProgress !== 'function') {
          return fetch('/api/tenant-sign/upload', {
            method: 'POST',
            headers: { 'Authorization': 'Bearer ' + localStorage.getItem(LS.access) },
            body: fd,
          }).then(async r => {
            const j = await r.json().catch(()=>({}));
            if (!r.ok) throw new Error(j.error || ('HTTP '+r.status));
            return j;
          });
        }
        return new Promise((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          xhr.open('POST', '/api/tenant-sign/upload');
          xhr.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem(LS.access));
          xhr.upload.onprogress = (e) => {
            if (e.lengthComputable) {
              const pct = Math.round((e.loaded / e.total) * 100);
              try { onProgress(pct); } catch (_) {}
            }
          };
          xhr.onload = () => {
            let j = {};
            try { j = JSON.parse(xhr.responseText || '{}'); } catch (_) {}
            if (xhr.status >= 200 && xhr.status < 300) resolve(j);
            else reject(new Error(j.error || ('HTTP ' + xhr.status)));
          };
          xhr.onerror = () => reject(new Error('Сетевая ошибка при загрузке'));
          xhr.onabort = () => reject(new Error('Загрузка отменена'));
          xhr.send(fd);
        });
      },
      send: (id, body) => post('/api/tenant-sign/' + id + '/send', body || {}),
      sendEmail: (id, body) => post('/api/tenant-sign/' + id + '/send-email', body || {}),
      cancel: (id, body) => post('/api/tenant-sign/' + id + '/cancel', body || {}),
      delete: (id) => del('/api/tenant-sign/' + id),
      executorPdfUrl: (id) => '/api/tenant-sign/' + id + '/file/executor',
      signedPdfUrl: (id) => '/api/tenant-sign/' + id + '/file/signed',
    },
    halls: {
      list: () => get('/api/halls'),
      create: (d) => post('/api/halls', d),
      update: (id, d) => patch('/api/halls/' + id, d),
      delete: (id) => del('/api/halls/' + id),
      // Залам можно отправить ДВА файла: оригинал (photo) для лайтбокса +
      // кропнутый кружок (thumb) для аватарки. Если передан только один
      // аргумент — он идёт как `photo`, бэкенд сам сгенерирует thumb через
      // sharp.center-crop.
      uploadPhoto: (id, photoFile, thumbBlob) => {
        const token = localStorage.getItem(LS.access);
        const fd = new FormData();
        const photoName = (photoFile && photoFile.name) || 'photo.jpg';
        fd.append('photo', photoFile, photoName);
        if (thumbBlob) fd.append('thumb', thumbBlob, 'thumb.jpg');
        return fetch(`/api/halls/${id}/photo`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${token}` },
          body: fd,
        }).then(async r => {
          if (!r.ok) { let err = {}; try { err = await r.json(); } catch {}
            throw new Error(err.error || `HTTP ${r.status}`); }
          return r.json();
        });
      },
      deletePhoto: (id) => del(`/api/halls/${id}/photo`),
    },
    reference: {
      tags: () => get('/api/reference/tags'),
      tagCreate: (name, color) => post('/api/reference/tags', { name, color }),
      tagUpdate: (id, d) => patch('/api/reference/tags/' + id, d),
      tagDelete: (id) => del('/api/reference/tags/' + id),
      statuses: () => get('/api/reference/client-statuses'),
      sources: () => get('/api/reference/client-sources'),
      sourceCreate: (name) => post('/api/reference/client-sources', { name }),
      sourceUpdate: (id, name) => patch('/api/reference/client-sources/' + id, { name }),
      sourceDelete: (id) => del('/api/reference/client-sources/' + id),
      branches: () => get('/api/branches'),
      disciplines: () => get('/api/disciplines'),
      disciplineCreate: (name) => post('/api/disciplines', { name }),
      disciplineUpdate: (id, d) => patch('/api/disciplines/' + id, d),
      disciplineDelete: (id) => del('/api/disciplines/' + id),
      paymentMethods: (params) => get('/api/payment-methods' + buildQs(params)),
    },
    max: {
      status: () => get('/api/max/status'),
      configToggle: (enabled) => post('/api/max/config/toggle', { enabled }),
      unreadCount: () => get('/api/max/unread-count'),
      startLogin: () => post('/api/max/auth/start-login'),
      cancelLogin: () => post('/api/max/auth/cancel-login'),
      logout: () => post('/api/max/auth/logout'),
      wipe: () => post('/api/max/auth/wipe'),
      qrUrl: (tick) => {
        const t = localStorage.getItem(LS.access);
        const cb = (tick != null) ? tick : Date.now();
        return `/api/max/auth/qr.png?t=${cb}&token=${encodeURIComponent(t || '')}`;
      },
      chats: () => get('/api/max/chats'),
      sync: () => post('/api/max/chats/sync'),
      refreshContacts: (all = false) => post('/api/max/chats/refresh-contacts' + (all ? '?all=1' : '')),
      messages: (chatId, params) => get(`/api/max/chats/${chatId}/messages` + buildQs(params)),
      loadHistory: (chatId, body) => post(`/api/max/chats/${chatId}/history`, body || { count: 50 }),
      send: (chatId, text, reply_to) => post(`/api/max/chats/${chatId}/send`, { text, reply_to }),
      read: (chatId) => post(`/api/max/chats/${chatId}/read`),
      // body — либо { client_id } / { teacher_id }, либо просто число (legacy = client_id)
      bindContact: (maxUserId, body) => post(`/api/max/contacts/${maxUserId}/bind`,
        body && typeof body === 'object' ? body : { client_id: body }),
      unbindContact: (maxUserId) => post(`/api/max/contacts/${maxUserId}/unbind`),
      archiveChat: (chatId, archived = true) => post(`/api/max/chats/${chatId}/archive`, { archived }),
      renameChat: (chatId, title) => post(`/api/max/chats/${chatId}/rename`, { title }),
      deleteChat: (chatId) => del(`/api/max/chats/${chatId}`),
      attachmentsByChat: (chatId) => get(`/api/max/chats/${chatId}/attachments`),
      attachmentUrl: (id) => {
        const t = localStorage.getItem(LS.access);
        return `/api/max/attachments/${id}/file?token=${encodeURIComponent(t || '')}`;
      },
      saveAttachmentToClient: (id) => post(`/api/max/attachments/${id}/save-to-client`),
      onlyofficePrepare: (id) => post(`/api/max/attachments/${id}/onlyoffice-prepare`),
      sendFile: (chatId, file, text) => {
        const token = localStorage.getItem(LS.access);
        const fd = new FormData();
        fd.append('file', file);
        if (text) fd.append('text', text);
        return fetch(`/api/max/chats/${chatId}/send-file`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${token}` },
          body: fd,
        }).then(async r => {
          if (!r.ok) {
            let err = {}; try { err = await r.json(); } catch {}
            throw new Error(err.error || `HTTP ${r.status}`);
          }
          return r.json();
        });
      },
      poll: (since, signal) => {
        const token = localStorage.getItem(LS.access);
        return fetch(`/api/max/events/poll?since=${since || 0}&timeout=25`, {
          headers: { Authorization: `Bearer ${token}` },
          signal,
        }).then(r => r.ok ? r.json() : Promise.reject(new Error('poll failed')));
      },
    },
    telegram: {
      status: () => get('/api/telegram/status'),
      configToggle: (enabled) => post('/api/telegram/config/toggle', { enabled }),
      // Auth flow: phone → code → (optional) 2FA. Каждый шаг отвечает state.
      startPhone: (phone) => post('/api/telegram/auth/start-phone', { phone }),
      submitCode: (code) => post('/api/telegram/auth/submit-code', { code }),
      submit2fa: (password) => post('/api/telegram/auth/submit-2fa', { password }),
      cancelAuth: () => post('/api/telegram/auth/cancel'),
      logout: () => post('/api/telegram/auth/logout'),
      unreadCount: () => get('/api/telegram/unread-count'),
      chats: () => get('/api/telegram/chats'),
      sync: () => post('/api/telegram/chats/sync'),
      messages: (chatId, params) => get(`/api/telegram/chats/${chatId}/messages` + buildQs(params)),
      loadHistory: (chatId, body) => post(`/api/telegram/chats/${chatId}/history`, body || { limit: 50 }),
      send: (chatId, text, reply_to) => post(`/api/telegram/chats/${chatId}/send`, { text, reply_to }),
      read: (chatId) => post(`/api/telegram/chats/${chatId}/read`),
      archiveChat: (chatId, archived = true) => post(`/api/telegram/chats/${chatId}/archive`, { archived }),
      deleteChat: (chatId) => del(`/api/telegram/chats/${chatId}`),
      bindContact: (tgUserId, body) => post(`/api/telegram/contacts/${tgUserId}/bind`,
        body && typeof body === 'object' ? body : { client_id: body }),
      unbindContact: (tgUserId) => post(`/api/telegram/contacts/${tgUserId}/unbind`),
      acceptSuggestion:  (tgUserId) => post(`/api/telegram/contacts/${tgUserId}/accept-suggestion`),
      declineSuggestion: (tgUserId) => post(`/api/telegram/contacts/${tgUserId}/decline-suggestion`),
      refreshContacts: () => post('/api/telegram/chats/refresh-contacts'),
      onlyofficePrepare: (id) => post(`/api/telegram/attachments/${id}/onlyoffice-prepare`),
      avatarUrl: (tgUserId) => {
        const t = localStorage.getItem(LS.access);
        return `/api/telegram/contacts/${tgUserId}/avatar?token=${encodeURIComponent(t || '')}`;
      },
      // Attachments
      attachmentsByChat: (chatId) => get(`/api/telegram/chats/${chatId}/attachments`),
      attachmentUrl: (id) => {
        const t = localStorage.getItem(LS.access);
        return `/api/telegram/attachments/${id}/file?token=${encodeURIComponent(t || '')}`;
      },
      sendFile: (chatId, file, text) => {
        const token = localStorage.getItem(LS.access);
        const fd = new FormData();
        fd.append('file', file);
        if (text) fd.append('text', text);
        return fetch(`/api/telegram/chats/${chatId}/send-file`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${token}` },
          body: fd,
        }).then(async r => {
          if (!r.ok) {
            let err = {}; try { err = await r.json(); } catch {}
            throw new Error(err.error || `HTTP ${r.status}`);
          }
          return r.json();
        });
      },
      // Long-poll событий sidecar→БД→фронт
      poll: (since, signal) => {
        const token = localStorage.getItem(LS.access);
        return fetch(`/api/telegram/events/poll?since=${since || 0}&timeout=25`, {
          headers: { Authorization: `Bearer ${token}` },
          signal,
        }).then(r => r.ok ? r.json() : Promise.reject(new Error('poll failed')));
      },
      eventsLastId: () => get('/api/telegram/events/last-id'),
    },
    yookassa: {
      getSettings: () => get('/api/yookassa/settings'),
      updateSettings: (d) => req('PUT', '/api/yookassa/settings', d),
      test: () => post('/api/yookassa/test'),
      createPaymentLink: (d) => post('/api/yookassa/payment-link', d),
      previewPaymentLink: (q) => {
        const qs = new URLSearchParams(q).toString();
        return get('/api/yookassa/payment-link/preview' + (qs ? '?' + qs : ''));
      },
      paymentLinks: (clientId) => get('/api/yookassa/payment-links' + (clientId ? `?client_id=${clientId}` : '')),
      refreshLink: (id) => post(`/api/yookassa/payment-links/${id}/refresh`),
      sendLinkEmail: (id) => post(`/api/yookassa/payment-links/${id}/send-email`),
    },
    settings: {
      all: () => get('/api/settings'),
      get: (key) => get('/api/settings/' + encodeURIComponent(key)),
      set: (key, value) => req('PUT', '/api/settings/' + encodeURIComponent(key), { value }),
      recomputeStatuses: () => post('/api/settings/recompute-statuses'),
    },
    operatorAlerts: {
      get: () => get('/api/operator-alerts'),
      dismiss: (alert_type, alert_key) => post('/api/operator-alerts/dismiss', { alert_type, alert_key }),
    },
    lkBroadcast: {
      send: (d) => post('/api/lk-broadcast', d),
      previewCount: (q) => get('/api/lk-broadcast/audience-preview' + buildQs(q)),
    },
    lkAnnouncements: {
      list: () => get('/api/lk-announcements'),
      create: (d) => post('/api/lk-announcements', d),
      update: (id, d) => req('PUT', `/api/lk-announcements/${id}`, d),
      remove: (id) => del(`/api/lk-announcements/${id}`),
      uploadImage: async (file) => {
        const fd = new FormData();
        fd.append('image', file);
        const token = localStorage.getItem(LS.access);
        const r = await fetch('/api/lk-announcements/upload-image', {
          method: 'POST',
          headers: token ? { Authorization: 'Bearer ' + token } : {},
          body: fd,
        });
        if (!r.ok) {
          let err = {};
          try { err = await r.json(); } catch {}
          throw new Error(err.error || ('HTTP ' + r.status));
        }
        return r.json();
      },
    },
    messages: {
      // SMS/MAX/Telegram отправляются профильными API (sms.sendToClient,
      // max.send, telegram.send). Здесь — только Email и хелперы.
      channels: (clientId) => get(`/api/sign/client-channels/${clientId}`),
      sendEmail: (clientId, text, subject) =>
        post('/api/messages/email-to-client',
          { client_id: clientId, text, ...(subject ? { subject } : {}) }),
      byClient: (clientId, limit) =>
        get(`/api/messages/by-client/${clientId}` + (limit ? `?limit=${limit}` : '')),
      log: (data) => post('/api/messages/log', data),
    },
    sms: {
      health: () => get('/api/sms/health'),
      sendToClient: (clientId, text) => post('/api/sms/send-to-client', { client_id: clientId, text }),
      sendToPhone: (to, text, clientId) => post('/api/sms/send-to-phone',
        { to, text, ...(clientId ? { client_id: clientId } : {}) }),
      byClient: (clientId, limit) => get(`/api/sms/by-client/${clientId}` + (limit ? `?limit=${limit}` : '')),
      templates: () => get('/api/sms/templates'),
      createTemplate: (d) => post('/api/sms/templates', d),
      updateTemplate: (id, d) => req('PUT', `/api/sms/templates/${id}`, d),
      deleteTemplate: (id) => del(`/api/sms/templates/${id}`),
      settings: () => get('/api/sms/settings'),
      saveSettings: (d) => req('PUT', '/api/sms/settings', d),
      sendTest: (d) => post('/api/sms/test', d),
      webhooks: () => get('/api/sms/webhooks'),
      registerWebhooks: (webhook_url) => post('/api/sms/webhooks/register', webhook_url ? { webhook_url } : {}),
      deleteWebhook: (id) => del(`/api/sms/webhooks/${encodeURIComponent(id)}`),
      resetDevice: () => post('/api/sms/reset-device'),
    },
    sign: {
      organizations: {
        list: () => get('/api/sign/organizations'),
        create: (d) => post('/api/sign/organizations', d),
        update: (id, d) => req('PUT', `/api/sign/organizations/${id}`, d),
        remove: (id) => del(`/api/sign/organizations/${id}`),
      },
      templates: {
        list: (p) => get('/api/sign/templates' + buildQs(p)),
        create: (d) => post('/api/sign/templates', d),
        update: (id, d) => req('PUT', `/api/sign/templates/${id}`, d),
        remove: (id) => del(`/api/sign/templates/${id}`),
        versions: (id) => get(`/api/sign/templates/${id}/versions`),
        updateVersion: (id, vid, d) =>
          req('PUT', `/api/sign/templates/${id}/versions/${vid}`, d),
        uploadVersion: async (id, file, notes, extra) => {
          const fd = new FormData();
          fd.append('file', file);
          if (notes) fd.append('notes', notes);
          if (extra && extra.offer_published_at) {
            fd.append('offer_published_at', extra.offer_published_at);
          }
          const token = localStorage.getItem(LS.access);
          const r = await fetch(`/api/sign/templates/${id}/upload-version`, {
            method: 'POST',
            headers: token ? { Authorization: 'Bearer ' + token } : {},
            body: fd,
          });
          const body = await r.json().catch(() => ({}));
          if (!r.ok) throw new Error(body.error || body.message || 'upload failed');
          return body;
        },
        downloadVersionUrl: (id, vid) => `/api/sign/templates/${id}/versions/${vid}/download`,
        downloadVersion: async (id, vid, filename) => {
          const token = localStorage.getItem(LS.access);
          const r = await fetch(`/api/sign/templates/${id}/versions/${vid}/download`, {
            headers: token ? { Authorization: 'Bearer ' + token } : {},
          });
          if (!r.ok) {
            const body = await r.json().catch(() => ({}));
            throw new Error(body.error || body.message || `HTTP ${r.status}`);
          }
          const blob = await r.blob();
          const url = URL.createObjectURL(blob);
          const a = document.createElement('a');
          a.href = url;
          a.download = filename || `template-${id}-v${vid}.docx`;
          document.body.appendChild(a);
          a.click();
          a.remove();
          setTimeout(() => URL.revokeObjectURL(url), 1000);
        },
      },
      sendToClient: (d) => post('/api/sign/send-to-client', d),
      clientChannels: (id) => get(`/api/sign/client-channels/${id}`),
      requests: {
        list: (p) => get('/api/sign/requests' + buildQs(p)),
        detail: (id) => get(`/api/sign/requests/${id}`),
        resend: (id) => post(`/api/sign/requests/${id}/resend-sms`),
        cancel: (id) => post(`/api/sign/requests/${id}/cancel`),
        verify: (id) => get(`/api/sign/requests/${id}/verify`),
      },
      settings: {
        get: () => get('/api/sign/settings'),
        save: (d) => req('PUT', '/api/sign/settings', d),
      },
      applications: {
        initiate: (d) => post('/api/sign/applications/initiate', d),
        list: (p) => get('/api/sign/applications' + buildQs(p)),
        detail: (id) => get(`/api/sign/applications/${id}`),
      },
    },
    lifepay: {
      getSettings: () => get('/api/lifepay/settings'),
      updateSettings: (d) => req('PUT', '/api/lifepay/settings', d),
      receipts: (p) => get('/api/lifepay/receipts' + buildQs(p)),
      receipt: (id) => get('/api/lifepay/receipts/' + id),
      retry: (id) => post(`/api/lifepay/receipts/${id}/retry`),
      refund: (txId, reason) => post(`/api/lifepay/transactions/${txId}/refund`, { reason }),
      testPing: (d) => post('/api/lifepay/test-ping', d || {}),
    },
    auditLog: {
      list: (p) => get('/api/audit-log' + buildQs(p)),
      users: () => get('/api/audit-log/users'),
      settings: () => get('/api/audit-log/settings'),
      saveSettings: (d) => req('PUT', '/api/audit-log/settings', d),
      cleanup: () => post('/api/audit-log/cleanup'),
    },
    assistant: {
      getConfig:       () => get('/api/assistant/config'),
      saveConfig:      (d) => req('PUT', '/api/assistant/config', d),
      kbList:          () => get('/api/assistant/kb'),
      kbCreate:        (d) => post('/api/assistant/kb', d),
      kbUpdate:        (id, d) => req('PATCH', `/api/assistant/kb/${id}`, d),
      kbDelete:        (id) => del(`/api/assistant/kb/${id}`),
      test:            (text) => post('/api/assistant/test', { text }),
      resetHistory:    (channel, chat_id) => post('/api/assistant/reset-history', { channel, chat_id }),
      viewHistory:     (channel, chat_id) => get('/api/assistant/history' + buildQs({ channel, chat_id })),
      audit:           (params) => get('/api/assistant/audit' + buildQs(params || {})),
      autoResets:      (params) => get('/api/assistant/auto-resets' + buildQs(params || {})),
      dismissAutoReset:(id) => post(`/api/assistant/auto-resets/${id}/dismiss`),
    },
    bot: {
      settings:        () => get('/api/bot/settings'),
      saveSettings:    (d) => req('PUT', '/api/bot/settings', d),
      reports:         () => get('/api/bot/reports'),
      updateReport:    (id, d) => req('PATCH', `/api/bot/reports/${id}`, d),
      runReport:       (id) => post(`/api/bot/reports/${id}/run`),
      subscribers:     () => get('/api/bot/subscribers'),
      addSubscriber:   (d) => post('/api/bot/subscribers', d),
      updateSubscriber:(id, d) => req('PATCH', `/api/bot/subscribers/${id}`, d),
      deleteSubscriber:(id) => del(`/api/bot/subscribers/${id}`),
      logs:            (limit=200) => get(`/api/bot/logs?limit=${limit}`),
      clearLogs:       () => del('/api/bot/logs'),
      test:            (chat_id, text) => post('/api/bot/test', { chat_id, text }),
      stats:           () => get('/api/bot/stats'),
      gigachatBalance: () => get('/api/bot/gigachat-balance'),
    },
    clientFiles: {
      list: (clientId) => get(`/api/clients/${clientId}/files`),
      upload: (clientId, file) => {
        const token = localStorage.getItem(LS.access);
        const fd = new FormData();
        fd.append('file', file);
        return fetch(`/api/clients/${clientId}/files`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${token}` },
          body: fd,
        }).then(async r => {
          if (!r.ok) {
            let err = {}; try { err = await r.json(); } catch {}
            throw new Error(err.error || `HTTP ${r.status}`);
          }
          return r.json();
        });
      },
      url: (clientId, fileId) => {
        const t = localStorage.getItem(LS.access);
        return `/api/clients/${clientId}/files/${fileId}?token=${encodeURIComponent(t || '')}`;
      },
      delete: (clientId, fileId) => del(`/api/clients/${clientId}/files/${fileId}`),
    },
    mail: {
      me:           () => get('/api/mail/me'),
      settings:     () => get('/api/mail/settings'),
      saveSettings: (d) => req('PATCH', '/api/mail/settings', d),
      testConn:     () => post('/api/mail/test-connection'),
      folders:      () => get('/api/mail/folders'),
      createFolder: (path) => post('/api/mail/folders', { path }),
      renameFolder: (from, to) => req('PATCH', '/api/mail/folders', { from, to }),
      deleteFolder: (path) => req('DELETE', '/api/mail/folders', { path }),
      messages:     ({ folder='INBOX', page=1, limit=50, search='', sort='date_desc' } = {}) => {
        const q = new URLSearchParams({ folder, page, limit, search, sort });
        return get(`/api/mail/messages?${q}`);
      },
      message:      (uid, folder='INBOX') => get(`/api/mail/message/${uid}?folder=${encodeURIComponent(folder)}`),
      attachmentUrl:(uid, idx, folder='INBOX') => {
        const t = localStorage.getItem(LS.access);
        return `/api/mail/attachment/${uid}/${idx}?folder=${encodeURIComponent(folder)}&token=${encodeURIComponent(t || '')}`;
      },
      send:         (d) => post('/api/mail/send', d),
      move:         (uid, from, to) => post('/api/mail/move', { uid, from, to }),
      moveMany:     (uids, from, to) => post('/api/mail/move-many', { uids, from, to }),
      remove:       (uid, folder, permanent=false) => post('/api/mail/delete', { uid, folder, permanent }),
      removeMany:   (uids, folder, permanent=false) => post('/api/mail/delete-many', { uids, folder, permanent }),
      flag:         (uid, folder, flag, add) => post('/api/mail/flag', { uid, folder, flag, add }),
      flagMany:     (uids, folder, flag, add) => post('/api/mail/flag-many', { uids, folder, flag, add }),
      unread:       () => get('/api/mail/unread'),
      poll:         (since) => get(`/api/mail/poll?since=${since || 0}`),
      contacts:     () => get('/api/mail/contacts'),
      addContact:   (email, name) => post('/api/mail/contacts', { email, name }),
      searchAll:    (q, limit=30) => get(`/api/mail/search-all?q=${encodeURIComponent(q)}&limit=${limit}`),
      onlyofficePrepare: (uid, idx, folder='INBOX') => post('/api/mail/onlyoffice/prepare', { uid, idx, folder }),
      sieveList:    () => get('/api/mail/sieve'),
      sieveSave:    (rules) => req('PUT', '/api/mail/sieve', { rules }),
      sieveAddRule: ({ type, value, folder, applyExisting=true }) => post('/api/mail/sieve/rule', { type, value, folder, applyExisting }),
      threads:      ({ folder='INBOX', page=1, limit=50 } = {}) => {
        const q = new URLSearchParams({ folder, page, limit });
        return get(`/api/mail/threads?${q}`);
      },
      pushSubscribe:   (sub) => post('/api/mail/push/subscribe', sub),
      pushUnsubscribe: (endpoint) => post('/api/mail/push/unsubscribe', { endpoint }),
      pushVapidKey:    () => get('/api/mail/push/vapid-key'),
    },
  };
})();
