/* ============================================================
   Data layer — mock store, series, waterfall, SKUs, operator
   All amounts in ₽. One store: «Дом и уют» (home goods seller)
   ============================================================ */

const RU = new Intl.NumberFormat('ru-RU');
const fmt    = (n) => RU.format(Math.round(n));
const fmtRub = (n) => RU.format(Math.round(n)) + ' ₽';
const fmtK   = (n) => {
  if (Math.abs(n) >= 1e6) return (n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1).replace('.', ',') + ' млн';
  if (Math.abs(n) >= 1e3) return Math.round(n / 1e3) + ' тыс';
  return RU.format(n);
};
const fmtPct = (n, signed) => (signed && n > 0 ? '+' : '') + n.toLocaleString('ru-RU', { maximumFractionDigits: 1 }) + '%';

const STORE = { name: 'Загрузка...', sub: '', note: '' };

/* ---- Period model: hero "пульс + итог" --------------------- */
const PERIODS = [
  { id: 'day',    label: 'День' },
  { id: 'week',   label: 'Неделя' },
  { id: 'month',  label: 'Месяц' },
  { id: 'custom', label: 'Период' },
];

// Русские названия месяцев для выбора конкретного месяца (MonthPicker)
const MONTHS_RU = [
  'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
  'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь',
];

// "Заработано (расчётно, нарастающим итогом)" per period + delta vs same point last period
const HERO = {
  day:    { eyebrow: 'Заработано сегодня · расчётно',       value: 0,  delta: 0,   compare: 'к этому дню недели',          sub: 'Нет данных' },
  week:   { eyebrow: 'Заработано за неделю · расчётно',     value: 0,  delta: 0,  compare: 'к этой же неделе мес. назад', sub: 'Нет данных' },
  month:  { eyebrow: 'Заработано в этом месяце · расчётно', value: 0,  delta: 0,  compare: 'к 5 числу прошлого месяца',   sub: 'Нет данных' },
  custom: { eyebrow: 'Заработано за период · расчётно',     value: 0,  delta: 0,  compare: '', sub: 'Выберите даты' },
  // Месяц без данных: рантайм-пересчёт этот ключ НЕ трогает — всегда нули
  empty:  { eyebrow: 'Заработано за месяц · расчётно',       value: 0,  delta: 0,  compare: '', sub: 'Нет данных за этот месяц' },
};

// Calculate previous month dynamically
const _prevMonthDate = new Date(Date.now() - 30 * 24 * 3600 * 1000);
const _prevMonthName = _prevMonthDate.toLocaleString('ru-RU', { month: 'long' });
const FINALIZED = { value: 0, month: _prevMonthName.charAt(0).toUpperCase() + _prevMonthName.slice(1), delta: 0 };

/* ---- Daily series (June 1–5 highlighted; trailing context) -- */
// profit per day, ₽ — used for hero spark + Деньги dynamics
const DAILY = Array.from({ length: 17 }, (_, i) => {
  const d = new Date();
  d.setDate(d.getDate() - (16 - i));
  const dd = String(d.getDate()).padStart(2, '0');
  const mm = String(d.getMonth() + 1).padStart(2, '0');
  return { d: `${dd}.${mm}`, v: 0 };
});

// revenue (выручка) series for Деньги — parallel to DAILY
const REVENUE = Array.from({ length: 17 }, () => 0);

/* ---- Platform breakdown (month-to-date) -------------------- */
const PLATFORMS = [
  { id: 'wb',   name: 'Вайлдберриз', short: 'Wildberries', glyph: 'WB', cls: 'wb',   value: 0, delta: 0,  share: 0 },
  { id: 'ozon', name: 'Озон',        short: 'Ozon', glyph: 'OZ', cls: 'ozon', value: 0, delta: 0,  share: 0 },
  { id: 'ya',   name: 'Яндекс Маркет', short: 'Яндекс Маркет', glyph: 'ЯМ', cls: 'ya',   value: 0,  delta: 0, share: 0 },
];

/* ---- Per-period platform breakdown --------------------------------- */
const PLATFORMS_BY_PERIOD = {
  day: [
    { id: 'wb',   name: 'Вайлдберриз',   short: 'Wildberries',    glyph: 'WB', cls: 'wb',   value: 0, delta: 0, share: 0 },
    { id: 'ozon', name: 'Озон',           short: 'Ozon',           glyph: 'OZ', cls: 'ozon', value: 0, delta: 0, share: 0 },
    { id: 'ya',   name: 'Яндекс Маркет', short: 'Яндекс Маркет', glyph: 'ЯМ', cls: 'ya',   value: 0, delta: 0, share: 0 },
  ],
  week: [
    { id: 'wb',   name: 'Вайлдберриз',   short: 'Wildberries',    glyph: 'WB', cls: 'wb',   value: 0, delta: 0, share: 0 },
    { id: 'ozon', name: 'Озон',           short: 'Ozon',           glyph: 'OZ', cls: 'ozon', value: 0, delta: 0, share: 0 },
    { id: 'ya',   name: 'Яндекс Маркет', short: 'Яндекс Маркет', glyph: 'ЯМ', cls: 'ya',   value: 0, delta: 0, share: 0 },
  ],
  month:  PLATFORMS,
  custom: PLATFORMS,
  // Месяц без данных — отдельные нулевые объекты (не перезаписываются пересчётом)
  empty: [
    { id: 'wb',   name: 'Вайлдберриз',   short: 'Wildberries',    glyph: 'WB', cls: 'wb',   value: 0, delta: 0, share: 0 },
    { id: 'ozon', name: 'Озон',           short: 'Ozon',           glyph: 'OZ', cls: 'ozon', value: 0, delta: 0, share: 0 },
    { id: 'ya',   name: 'Яндекс Маркет', short: 'Яндекс Маркет', glyph: 'ЯМ', cls: 'ya',   value: 0, delta: 0, share: 0 },
  ],
};

/* ---- Раздел «Продажи»: 28 дней по площадкам ---------------- */
// Last-28-days daily sales in ₽ per platform + totals & units
const _days28 = (() => {
  const out = [];
  const end = new Date();
  for (let i = 27; i >= 0; i--) {
    const dt = new Date(end); dt.setDate(end.getDate() - i);
    out.push(String(dt.getDate()).padStart(2, '0') + '.' + String(dt.getMonth() + 1).padStart(2, '0'));
  }
  return out;
})();

const SALES_28D = {
  wb:   { series: _days28.map(d => ({ v: 0, d })), units: 0, delta: 0, total: 0 },
  ozon: { series: _days28.map(d => ({ v: 0, d })), units: 0, delta: 0, total: 0 },
  ya:   { series: _days28.map(d => ({ v: 0, d })), units: 0, delta: 0, total: 0 },
};

/* ---- Per-period sales series -------------------------------------- */
const SALES_BY_PERIOD = {
  day: {
    wb:   { series: [], total: 0, units: 0, delta: 0 },
    ozon: { series: [], total: 0, units: 0, delta: 0 },
    ya:   { series: [], total: 0, units: 0, delta: 0 },
  },
  week: {
    wb:   { series: [], total: 0, units: 0, delta: 0 },
    ozon: { series: [], total: 0, units: 0, delta: 0 },
    ya:   { series: [], total: 0, units: 0, delta: 0 },
  },
  month: {
    wb:   { series: [], total: 0, units: 0, delta: 0 },
    ozon: { series: [], total: 0, units: 0, delta: 0 },
    ya:   { series: [], total: 0, units: 0, delta: 0 },
  },
  custom: {
    wb:   { series: [], total: 0, units: 0, delta: 0 },
    ozon: { series: [], total: 0, units: 0, delta: 0 },
    ya:   { series: [], total: 0, units: 0, delta: 0 },
  },
  // Месяц без данных — нули (пересчёт этот ключ не трогает)
  empty: {
    wb:   { series: [], total: 0, units: 0, delta: 0 },
    ozon: { series: [], total: 0, units: 0, delta: 0 },
    ya:   { series: [], total: 0, units: 0, delta: 0 },
  }
};

/* ---- Hero раскрытие: прозрачность точности ------------------ */
const ACCURACY = {
  confirmed: 0,   // подтверждено из фин-отчётов
  pending:   0,    // отгружено, отчёт ещё не пришёл
  share_confirmed: 0,
  items: [
    { label: 'Подтверждено фин-отчётами', value: 0, tone: 'green', note: 'Нет загруженных отчётов' },
    { label: 'Отгружено, ждём отчёт',      value: 0,  tone: 'amber', note: 'Нет текущих транзакций' },
    { label: 'Возможные корректировки',    value: 0,  tone: 'red',   note: '—' },
  ],
};

/* ---- Раздел «Деньги»: воронка месяца ----------------------- */
const WATERFALL = [
  { key: 'gross',  label: 'Выкуплено',                value: 0, kind: 'base' },
  { key: 'mp',     label: '− Расходы внутри МП',      value: 0, kind: 'sub', hint: 'Комиссия, логистика, хранение, штрафы' },
  { key: 'transfer', label: 'К перечислению',         value: 0, kind: 'mid' },
  { key: 'cost',   label: '− Себестоимость проданного', value: 0, kind: 'sub', hint: 'Из Excel оператора' },
  { key: 'tax',    label: '− Налог',                  value: 0, kind: 'sub', hint: 'УСН «Доходы» 6%' },
  { key: 'opex',   label: '− Операционные расходы',   value: 0, kind: 'sub', hint: 'Раскидано на период' },
  { key: 'profit', label: 'Чистая прибыль',           value: 0, kind: 'total' },
];

// Защитный сигнал: к перечислению vs себестоимость
const RECONCILE = {
  transfer: 0,
  cost: 0,
  ratio: 0,
  status: 'ok',
  margin_after_cost: 0,
};

// ДРР — отдельный виджет (зона маркетолога)
const DRR = {
  ad_spend: 0,
  ad_revenue: 0,
  drr: 0,
  target: 10,
  trend: [0, 0, 0, 0, 0],
};

const DRR_BY_PLATFORM = {
  all:  DRR,
  wb:   { ad_spend:  0, ad_revenue:  0, drr: 0, target: 10, trend: [0, 0, 0, 0, 0] },
  ozon: { ad_spend:  0, ad_revenue:  0, drr: 0, target: 11, trend: [0, 0, 0, 0, 0] },
  ya:   { ad_spend:  0, ad_revenue:  0, drr: 0, target: 12, trend: [0, 0, 0, 0, 0] },
};

/* ---- Воронка по площадкам ---------------------------------------- */
const WATERFALL_BY_PLATFORM = {
  all: WATERFALL,
  wb: [
    { key: 'gross',    label: 'Выкуплено',                  value:  0, kind: 'base' },
    { key: 'mp',       label: '− Расходы внутри МП',        value:  0, kind: 'sub',   hint: 'Комиссия, логистика, хранение, штрафы' },
    { key: 'transfer', label: 'К перечислению',             value:  0, kind: 'mid' },
    { key: 'cost',     label: '− Себестоимость проданного', value:  0, kind: 'sub',   hint: 'Из Excel оператора' },
    { key: 'tax',      label: '− Налог',                    value:   0, kind: 'sub',   hint: 'УСН «Доходы» 6%' },
    { key: 'opex',     label: '− Операционные расходы',     value:  0, kind: 'sub',   hint: 'Раскидано на период' },
    { key: 'profit',   label: 'Чистая прибыль',             value:   0, kind: 'total' },
  ],
  ozon: [
    { key: 'gross',    label: 'Выкуплено',                  value:  0, kind: 'base' },
    { key: 'mp',       label: '− Расходы внутри МП',        value:  0, kind: 'sub',   hint: 'Комиссия, логистика, хранение, штрафы' },
    { key: 'transfer', label: 'К перечислению',             value:   0, kind: 'mid' },
    { key: 'cost',     label: '− Себестоимость проданного', value:  0, kind: 'sub',   hint: 'Из Excel оператора' },
    { key: 'tax',      label: '− Налог',                    value:   0, kind: 'sub',   hint: 'УСН «Доходы» 6%' },
    { key: 'opex',     label: '− Операционные расходы',     value:   0, kind: 'sub',   hint: 'Раскидано на период' },
    { key: 'profit',   label: 'Чистая прибыль',             value:   0, kind: 'total' },
  ],
  ya: [
    { key: 'gross',    label: 'Выкуплено',                  value:   0, kind: 'base' },
    { key: 'mp',       label: '− Расходы внутри МП',        value:  0, kind: 'sub',   hint: 'Комиссия, логистика, хранение, штрафы' },
    { key: 'transfer', label: 'К перечислению',             value:   0, kind: 'mid' },
    { key: 'cost',     label: '− Себестоимость проданного', value:  0, kind: 'sub',   hint: 'Из Excel оператора' },
    { key: 'tax',      label: '− Налог',                    value:   0, kind: 'sub',   hint: 'УСН «Доходы» 6%' },
    { key: 'opex',     label: '− Операционные расходы',     value:   0, kind: 'sub',   hint: 'Раскидано на период' },
    { key: 'profit',   label: 'Чистая прибыль',             value:    0, kind: 'total' },
  ],
};

/* ---- ТОП-5 расходов по площадкам --------------------------------- */
const TOP_EXPENSES_BY_PLATFORM = {
  all: [],
  wb: [],
  ozon: [],
  ya: [],
};

/* ---- Раздел «Товары» --------------------------------------- */
const TOP_SKU = [];

const STOCK_RISK = [];

const MARGIN_DROP = [];

const ALL_PRODUCTS = [];

/* ---- Telegram-дайджест ------------------------------------- */
const DIGEST = {
  time: '08:30',
  date: 'сегодня, ' + new Date().toLocaleDateString('ru-RU', { day: 'numeric', month: 'long' }),
  yesterday: 0,
  deltaAvg: 0,
  alerts: 0,
};

/* ============================================================
   OPERATOR data
   ============================================================ */
const CABINETS = [];

const COST_ROWS = [];

const TAX_REGIMES = [
  { id: 'usn6',  name: 'УСН «Доходы»',         rate: '6%',   base: 'С выручки',          baseAmount: 0, tax: 0 },
  { id: 'usn15', name: 'УСН «Доходы − Расходы»', rate: '15%', base: 'С прибыли',          baseAmount: 0, tax: 0 },
  { id: 'patent',name: 'Патент',               rate: 'фикс', base: 'Фиксированный',      baseAmount: null,    tax: 0  },
  { id: 'osno',  name: 'ОСНО',                 rate: '—',    base: 'НДС + налог',        baseAmount: null,    tax: 0 },
];

const OPEX_ROWS = [];

/* ---- Операционные расходы по площадкам (по месяцам) ---------------
   Отдельные записи расходов, привязанные к кабинету площадки.
   Виджет показывает 5 последних (по дате), полный список — все за месяц. */
const OPEX_MONTHS = [
  { id: '2026-06', label: 'Июнь 2026', short: 'Июнь' },
  { id: '2026-05', label: 'Май 2026',  short: 'Май' },
];

const DEFAULT_OPEX_PLATFORM_TX = {
  '2026-06': {
    wb: [],
    ozon: [],
    ya: [],
  },
  '2026-05': {
    wb: [],
    ozon: [],
    ya: [],
  },
};

const RAW_TX = [];

const MANUAL_TAX = { patent: null, osno: null };

const VAT_RATE = 5;
const VAT_CONFIG = { enabled: false, base: 'revenue' };
const CUSTOM_TAX_CONFIG = { enabled: false, name: '', mode: 'percent', rate: 0, base: 'revenue', amount: 0 };

Object.assign(window, {
  RU, fmt, fmtRub, fmtK, fmtPct,
  STORE, PERIODS, HERO, FINALIZED, DAILY, REVENUE, PLATFORMS, SALES_28D,
  PLATFORMS_BY_PERIOD, SALES_BY_PERIOD,
  ACCURACY, WATERFALL, WATERFALL_BY_PLATFORM, TOP_EXPENSES_BY_PLATFORM, RECONCILE, DRR, DRR_BY_PLATFORM, TOP_SKU, STOCK_RISK, MARGIN_DROP, DIGEST,
  CABINETS, COST_ROWS, TAX_REGIMES, OPEX_ROWS, RAW_TX,
  OPEX_MONTHS,
  OPEX_PLATFORM_TX: JSON.parse(JSON.stringify(DEFAULT_OPEX_PLATFORM_TX)),
  ALL_PRODUCTS,
  TAX_SELECTED: ['usn6'],
  ACTIVE_MARKETPLACES: ['ozon', 'wb', 'ya'],
  MANUAL_TAX,
  VAT_RATE,
  VAT_CONFIG,
  CUSTOM_TAX_CONFIG,
  // Выбранный в шапке месяц ('YYYY-MM'), по которому фильтруются агрегации на
  // Главной/в Расчётах. Дефолт — текущий календарный месяц; App синхронизирует
  // его с пикером месяца (MonthPicker) при смене выбора.
  SELECTED_PERIOD: (() => {
    const d = new Date();
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
  })()
});

// Base values for dynamic scaling of daily sparklines / metrics
const BASE_DAILY = [
  { d: '20.05', v: 17800 }, { d: '21.05', v: 22400 }, { d: '22.05', v: 19100 },
  { d: '23.05', v: 24600 }, { d: '24.05', v: 26900 }, { d: '25.05', v: 15200 },
  { d: '26.05', v: 18700 }, { d: '27.05', v: 28100 }, { d: '28.05', v: 31200 },
  { d: '29.05', v: 24800 }, { d: '30.05', v: 22600 }, { d: '31.05', v: 29400 },
  { d: '01.06', v: 26300 }, { d: '02.06', v: 31900 }, { d: '03.06', v: 28700 },
  { d: '04.06', v: 21400 }, { d: '05.06', v: 33100, partial: true },
];
const BASE_REVENUE = [
  124000,142000,131000,158000,167000,98000,118000,176000,189000,
  152000,141000,182000,165000,198000,178000,138000,205000,
];

if (!window.BASE_DAILY) window.BASE_DAILY = JSON.parse(JSON.stringify(BASE_DAILY));
if (!window.BASE_REVENUE) window.BASE_REVENUE = JSON.parse(JSON.stringify(BASE_REVENUE));

// Период записи RAW_TX в формате 'YYYY-MM'. Источник истины — поле period (выбор
// пользователя при загрузке отчёта в OpRaw). Фолбэк — разбор tx.date 'DD.MM' для
// старых записей, сохранённых до появления поля period.
function periodOfTx(tx) {
  if (tx.period) return tx.period;
  if (tx.date && tx.date.split('.').length === 2) return `2026-${tx.date.split('.')[1]}`;
  return null;
}
window.periodOfTx = periodOfTx;

// RAW_TX, отфильтрованные по выбранному в шапке месяцу (window.SELECTED_PERIOD).
// Если период не задан (null) — возвращаются все записи. Используется всеми
// «текущий вид»-агрегациями (воронка, HERO, топ-SKU, топ-расходы и т.д.), чтобы
// цифры на Главной/в Расчётах относились именно к выбранному месяцу.
function rawTxForSelectedPeriod() {
  const p = window.SELECTED_PERIOD;
  const all = window.RAW_TX || [];
  return p ? all.filter(tx => periodOfTx(tx) === p) : all;
}
window.rawTxForSelectedPeriod = rawTxForSelectedPeriod;

// Один и тот же артикул продавец может завести в кабинетах Ozon/WB с разным
// регистром — используем как ключ группировки/сопоставления по SKU, чтобы
// такие записи не расходились в разные строки (см. ALL_PRODUCTS, TOP_SKU, costBySku).
function normalizeArticle(v) {
  return String(v || '').trim().toUpperCase();
}

// Rebuild TOP_SKU (Топ-5 товаров по выручке) from uploaded "Отчет по начислениям" reports
function recalculateTopSku() {
  const activePlats = window.ACTIVE_MARKETPLACES || ['ozon', 'wb', 'ya'];
  // Годятся любые отчёты с поартикульными данными: Ozon кладёт skus в «Отчет по
  // начислениям», WB — в «Отчет по продажам» (в начислениях WB skus нет).
  const reports = rawTxForSelectedPeriod().filter(tx =>
    (tx.type === 'Отчет по начислениям' || tx.type === 'Отчет по продажам') &&
    Array.isArray(tx.skus) && tx.skus.length && activePlats.includes(tx.plat));
  if (!reports.length) { TOP_SKU.length = 0; return; } // нет отчётов (в т.ч. после удаления последнего) — очищаем, не оставляем протухшие суммы

  const byPlat = {};
  reports.forEach(tx => {
    (byPlat[tx.plat] = byPlat[tx.plat] || []).push(tx);
  });

  const productMap = new Map(); // article -> { name, sku, rev, units, platforms: [] }

  Object.keys(byPlat).forEach(plat => {
    const sorted = byPlat[plat].slice().sort(
      (a, b) => new Date(a.report_meta?.date_to || 0) - new Date(b.report_meta?.date_to || 0)
    );
    const current = sorted[sorted.length - 1];
    const previous = sorted[sorted.length - 2];
    const prevByArticle = new Map((previous?.skus || []).map(s => [normalizeArticle(s.article), s]));

    (current.skus || []).forEach(s => {
      const rev = s.gross_revenue || 0;
      const units = Math.max(0, (s.sales_count || 0) - (s.returns_count || 0));
      const key = normalizeArticle(s.article);
      const prev = prevByArticle.get(key);
      const delta = prev && prev.gross_revenue ? (rev - prev.gross_revenue) / prev.gross_revenue * 100 : null;

      if (!productMap.has(key)) {
        productMap.set(key, { name: s.name, sku: s.article, rev: 0, units: 0, platforms: [] });
      }
      const p = productMap.get(key);
      p.rev += rev;
      p.units += units;
      p.platforms.push({ plat, rev, units, delta });
    });
  });

  const products = Array.from(productMap.values()).map(p => {
    const withDelta = p.platforms.filter(pl => pl.delta != null);
    const weight = withDelta.reduce((s, pl) => s + pl.rev, 0);
    const delta = weight > 0
      ? withDelta.reduce((s, pl) => s + pl.delta * pl.rev, 0) / weight
      : null;
    return { ...p, delta };
  });

  products.sort((a, b) => b.rev - a.rev);
  const top5 = products.slice(0, 5);

  TOP_SKU.length = 0;
  top5.forEach(p => TOP_SKU.push(p));
}

// Прибыль площадки для баз, считающихся "от прибыли" (УСН15, НДС, доп.налог)
window.platformProfit = (gross, mpAbs, costAbs, opexAbs) => gross - mpAbs - costAbs - opexAbs;

const TAX_SHORT_NAMES = { usn6: 'УСН 6%', usn15: 'УСН 15%', patent: 'Патент', osno: 'ОСНО' };

// Считает налог одной площадки: сумма выбранных базовых режимов + НДС (если включён) + доп.налог (если включён).
// ctx = {gross, mpAbs, costAbs, opexAbs} — показатели площадки (или компании при splitShare=1 для company-wide вызовов).
// splitShare — доля площадки в общей выручке, используется только для раскладки ФИКСИРОВАННЫХ сумм (patent/osno/custom-fixed),
// у которых нет собственной площадочной базы.
window.computeTaxForPlatform = (ctx, selectedIds, vatConfig, customConfig, splitShare) => {
  const { gross, mpAbs, costAbs, opexAbs } = ctx;
  const profit = () => Math.max(0, window.platformProfit(gross, mpAbs, costAbs, opexAbs));
  const breakdown = [];
  let total = 0;

  (selectedIds || []).forEach(id => {
    let value = 0;
    if (id === 'usn6') value = gross * 0.06;
    else if (id === 'usn15') value = profit() * 0.15;
    else if (id === 'patent' || id === 'osno') value = (window.MANUAL_TAX?.[id] || 0) * splitShare;
    else return;
    total += value;
    breakdown.push({ key: id, label: TAX_SHORT_NAMES[id] || id, value });
  });

  if (vatConfig?.enabled) {
    const base = vatConfig.base === 'profit' ? profit() : gross;
    const rate = window.VAT_RATE || 5;
    const value = base * (rate / (100 + rate));
    total += value;
    breakdown.push({ key: 'vat', label: `НДС ${window.VAT_RATE || 5}%`, value });
  }

  if (customConfig?.enabled) {
    let value = 0;
    if (customConfig.mode === 'fixed') {
      value = (customConfig.amount || 0) * splitShare;
    } else {
      const base = customConfig.base === 'profit' ? profit() : gross;
      value = base * ((customConfig.rate || 0) / 100);
    }
    total += value;
    breakdown.push({ key: 'custom', label: customConfig.name?.trim() || 'Доп. налог', value });
  }

  return { total, breakdown };
};

// Компактная составная метка для hint-подписей воронки и UI ("УСН 6% + НДС 5% + Реклам. сбор").
window.buildTaxHint = (selectedIds, vatConfig, customConfig) => {
  const parts = (selectedIds || []).map(id => TAX_SHORT_NAMES[id]).filter(Boolean);
  if (vatConfig?.enabled) parts.push(`НДС ${window.VAT_RATE || 5}%`);
  if (customConfig?.enabled) parts.push(customConfig.name?.trim() || 'Доп. налог');
  return { parts, full: parts.length ? parts.join(' + ') : 'Налог не выбран' };
};

// Dynamic Waterfall Recalculation
window.recalculateWaterfall = () => {
  // Reset expense buckets for every month currently known — защита от протухших/
  // задублированных записей при пересчёте, но без ограничения набора месяцев
  // фиксированным списком (см. OPEX_MONTHS ниже — набор месяцев динамический).
  if (window.OPEX_PLATFORM_TX) {
    for (const m of Object.keys(window.OPEX_PLATFORM_TX)) {
      window.OPEX_PLATFORM_TX[m] = { ozon: [], wb: [], ya: [] };
    }
  }

  const activePlats = window.ACTIVE_MARKETPLACES || ['ozon', 'wb', 'ya'];

  // Записи RAW_TX за выбранный в шапке месяц — для всех «текущий вид»-агрегаций
  // (воронка, HERO, топ-расходы, продажи, товары). Помесячная раскладка опекса
  // ниже (OPEX_PLATFORM_TX) сознательно идёт по ВСЕМ месяцам — экран «Опекс»
  // использует общий хэдерный MonthPicker, но читает свою помесячную раскладку
  // (OPEX_PLATFORM_TX/OPEX_MONTHS) напрямую, а не через SELECTED_PERIOD/viewTx.
  const viewTx = rawTxForSelectedPeriod();

  // Override from RAW_TX
  if (window.RAW_TX && window.OPEX_PLATFORM_TX) {
    const opexByMonthAndPlat = {};

    window.RAW_TX.forEach(tx => {
      // Filter by active platforms
      if (!activePlats.includes(tx.plat)) return;

      // Источник месяца — поле period ('YYYY-MM'), проставляемое при применении
      // отчёта в OpRaw (выбор пользователя, а не дата из файла). Фолбэк на разбор
      // tx.date — для записей, сохранённых до появления этого поля.
      let month = tx.period;
      if (!month) {
        const parts = tx.date.split('.');
        if (parts.length === 2) month = `2026-${parts[1]}`;
      }
      if (!month) return;

      if (!opexByMonthAndPlat[month]) {
        opexByMonthAndPlat[month] = { ozon: [], wb: [], ya: [] };
      }

      if (tx.type === 'Отчет по начислениям' && tx.opex_details) {
        tx.opex_details.forEach(item => {
          opexByMonthAndPlat[month][tx.plat].push({ ...item });
        });
      } else if (tx.amount < 0 && tx.type !== 'Отчет по начислениям') {
        let category = 'Прочее';
        if (tx.type === 'Комиссия') category = 'Прочее';
        else if (tx.type === 'Логистика') category = 'Логистика';
        else if (tx.type === 'Хранение') category = 'Хранение';
        else if (tx.type === 'Штраф') category = 'Штраф';
        else if (tx.type === 'Возврат') category = 'Прочее';

        opexByMonthAndPlat[month][tx.plat].push({
          date: tx.date,
          name: tx.type,
          tag: category,
          amount: Math.abs(tx.amount)
        });
      }
    });

    for (const month in opexByMonthAndPlat) {
      if (!window.OPEX_PLATFORM_TX[month]) {
        window.OPEX_PLATFORM_TX[month] = { ozon: [], wb: [], ya: [] };
      }
      for (const plat of ['ozon', 'wb', 'ya']) {
        const list = opexByMonthAndPlat[month][plat];
        if (list && list.length > 0) {
          const aggregated = {};
          list.forEach(item => {
            const key = item.name;
            if (!aggregated[key]) {
              aggregated[key] = { ...item };
            } else {
              aggregated[key].amount += item.amount;
            }
          });
          window.OPEX_PLATFORM_TX[month][plat] = Object.values(aggregated);
        } else {
          // Clean deactivated platforms' transaction details
          if (!activePlats.includes(plat)) {
            window.OPEX_PLATFORM_TX[month][plat] = [];
          }
        }
      }
    }

    // Бакет текущего реального месяца доступен всегда, даже без данных — чтобы у
    // компании без единой загрузки «Опекс» не выглядел совсем пустым.
    const _n = new Date();
    const currentPeriod = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, '0')}`;
    if (!window.OPEX_PLATFORM_TX[currentPeriod]) {
      window.OPEX_PLATFORM_TX[currentPeriod] = { ozon: [], wb: [], ya: [] };
    }

    // Пересобрать OPEX_MONTHS мутацией существующего массива на месте (как и
    // TOP_SKU в recalculateTopSku выше) — operator.jsx ссылается на него как на
    // голый идентификатор верхнего уровня общего лексического скоупа между
    // <script>-тегами, а не через window.*, поэтому переприсваивание не увидит.
    const sortedIds = Object.keys(window.OPEX_PLATFORM_TX).sort().reverse();
    OPEX_MONTHS.length = 0;
    sortedIds.forEach(id => {
      const [y, m] = id.split('-').map(Number);
      OPEX_MONTHS.push({ id, label: `${MONTHS_RU[m - 1]} ${y}`, short: MONTHS_RU[m - 1] });
    });
  }

  // ── A: gross & mp из RAW_TX ───────────────────────────────────────────
  const pGrossFromTx = { wb: 0, ozon: 0, ya: 0 };
  const pMpFromTx    = { wb: 0, ozon: 0, ya: 0 };
  const pUnitsFromTx = { wb: 0, ozon: 0, ya: 0 };
  const MP_EXPENSE_TYPES = new Set(['Комиссия', 'Логистика', 'Хранение', 'Штраф']);

  viewTx.forEach(tx => {
    if (!activePlats.includes(tx.plat)) return;
    if (tx.type === 'Отчет по начислениям') {
      const txGross = tx.gross_revenue ?? tx.report_meta?.store_revenue
        ?? (Array.isArray(tx.skus) ? tx.skus.reduce((s, sk) => s + (sk.gross_revenue || 0), 0) : 0);
      pGrossFromTx[tx.plat] += txGross;
      const exp = tx.report_meta?.store_expenses || {};
      pMpFromTx[tx.plat] -= Object.values(exp).reduce((s, v) => s + Math.abs(v || 0), 0);
      if (Array.isArray(tx.skus)) tx.skus.forEach(s => {
        pUnitsFromTx[tx.plat] += Math.max(0, (s.sales_count || 0) - (s.returns_count || 0));
      });
    } else if (tx.type === 'Продажа') {
      pGrossFromTx[tx.plat] += (tx.amount || 0);
      pUnitsFromTx[tx.plat] += (tx.qty || 0);
    } else if (tx.type === 'Возврат') {
      pGrossFromTx[tx.plat] += (tx.amount || 0);
      pUnitsFromTx[tx.plat] -= (tx.qty || 0);
    } else if (MP_EXPENSE_TYPES.has(tx.type)) {
      pMpFromTx[tx.plat] += (tx.amount || 0);
    }
  });

  for (const plat of ['wb', 'ozon', 'ya']) {
    const pl = window.WATERFALL_BY_PLATFORM[plat];
    if (pl) { 
      pl[0].value = Math.round(pGrossFromTx[plat]); 
      pl[1].value = Math.round(pMpFromTx[plat]); 
    }
  }

  const selectedIds = Array.isArray(window.TAX_SELECTED) ? window.TAX_SELECTED
    : (window.TAX_SELECTED ? [window.TAX_SELECTED] : []);
  const vatConfig = window.VAT_CONFIG || { enabled: false, base: 'revenue' };
  const customConfig = window.CUSTOM_TAX_CONFIG || { enabled: false, name: '', mode: 'percent', rate: 0, base: 'revenue', amount: 0 };
  const taxHint = window.buildTaxHint(selectedIds, vatConfig, customConfig).full;

  const splits = {
    wb:   { share: 0.509, tax: 0.51, opex: 0.51 },
    ozon: { share: 0.362, tax: 0.36, opex: 0.36 },
    ya:   { share: 0.129, tax: 0.13, opex: 0.13 },
  };

  // ── C: реальные доли из gross для splits ──────────────────────────────
  const realTotalGross = activePlats.reduce((sum, p) => {
    const pl = window.WATERFALL_BY_PLATFORM[p];
    return sum + (pl ? pl[0].value : 0);
  }, 0);
  if (realTotalGross > 0) {
    for (const plat of ['wb', 'ozon', 'ya']) {
      const pl = window.WATERFALL_BY_PLATFORM[plat];
      const share = pl ? pl[0].value / realTotalGross : 0;
      splits[plat].tax  = share;
      splits[plat].opex = share;
    }
  } else {
    const activeCount = activePlats.length || 1;
    for (const plat of ['wb', 'ozon', 'ya']) {
      const active = activePlats.includes(plat);
      const share = active ? (1 / activeCount) : 0;
      splits[plat].tax = share;
      splits[plat].opex = share;
    }
  }

  // Normalization for splits.opex
  let activeOpexSplitSum = 0;
  for (const plat of activePlats) {
    if (splits[plat]) {
      activeOpexSplitSum += splits[plat].opex;
    }
  }
  if (activeOpexSplitSum === 0) activeOpexSplitSum = 1.0;

  const platformUnits = {
    wb: 0,
    ozon: 0,
    ya: 0
  };

  // ── D: реальные units для 'unit'-opex ─────────────────────────────────
  for (const plat of ['wb', 'ozon', 'ya']) {
    if (pUnitsFromTx[plat] > 0) platformUnits[plat] = pUnitsFromTx[plat];
  }

  let totalUnits = 0;
  for (const plat of activePlats) {
    totalUnits += platformUnits[plat] || 0;
  }

  let totalOpexVal = 0;
  const pOpexVals = { wb: 0, ozon: 0, ya: 0 };

  (window.OPEX_ROWS || []).forEach(r => {
    const amt = r.amount || 0;
    r.computedAmount = 0;
    r.computedAmountsByPlat = { wb: 0, ozon: 0, ya: 0 };
    if (r.method === 'period') {
      if (r.plat && r.plat !== 'all') {
        if (activePlats.includes(r.plat)) {
          pOpexVals[r.plat] += amt;
          totalOpexVal += amt;
          r.computedAmount = amt;
          r.computedAmountsByPlat[r.plat] = amt;
        }
      } else {
        if (activePlats.includes('wb')) {
          const splitAmt = amt * (splits.wb.opex / activeOpexSplitSum);
          pOpexVals.wb += splitAmt;
          r.computedAmountsByPlat.wb = splitAmt;
        }
        if (activePlats.includes('ozon')) {
          const splitAmt = amt * (splits.ozon.opex / activeOpexSplitSum);
          pOpexVals.ozon += splitAmt;
          r.computedAmountsByPlat.ozon = splitAmt;
        }
        if (activePlats.includes('ya')) {
          const splitAmt = amt * (splits.ya.opex / activeOpexSplitSum);
          pOpexVals.ya += splitAmt;
          r.computedAmountsByPlat.ya = splitAmt;
        }
        totalOpexVal += amt;
        r.computedAmount = amt;
      }
    } else if (r.method === 'unit') {
      if (r.plat && r.plat !== 'all') {
        if (activePlats.includes(r.plat)) {
          const platAmt = amt * platformUnits[r.plat];
          pOpexVals[r.plat] += platAmt;
          totalOpexVal += platAmt;
          r.computedAmount = platAmt;
          r.computedAmountsByPlat[r.plat] = platAmt;
        }
      } else {
        let totalUnitAmt = 0;
        if (activePlats.includes('wb')) {
          const platAmt = amt * platformUnits.wb;
          pOpexVals.wb += platAmt;
          r.computedAmountsByPlat.wb = platAmt;
          totalUnitAmt += platAmt;
        }
        if (activePlats.includes('ozon')) {
          const platAmt = amt * platformUnits.ozon;
          pOpexVals.ozon += platAmt;
          r.computedAmountsByPlat.ozon = platAmt;
          totalUnitAmt += platAmt;
        }
        if (activePlats.includes('ya')) {
          const platAmt = amt * platformUnits.ya;
          pOpexVals.ya += platAmt;
          r.computedAmountsByPlat.ya = platAmt;
          totalUnitAmt += platAmt;
        }
        totalOpexVal += amt * totalUnits;
        r.computedAmount = totalUnitAmt;
      }
    }
  });

  let taxVal = 0;

  // ── B: себестоимость из COST_ROWS ────────────────────────────────────
  const costBySku = new Map();
  (window.COST_ROWS || []).forEach(r => { costBySku.set(normalizeArticle(r.sku), r.cost != null ? r.cost : 0); });

  const pCostFromRows = { wb: 0, ozon: 0, ya: 0 };
  viewTx.forEach(tx => {
    if (!activePlats.includes(tx.plat)) return;
    if ((tx.type === 'Отчет по начислениям' || tx.type === 'Отчет по продажам') && Array.isArray(tx.skus)) {
      tx.skus.forEach(s => {
        const soldQty = Math.max(0, (s.sales_count || 0) - (s.returns_count || 0));
        pCostFromRows[tx.plat] += (costBySku.get(normalizeArticle(s.article)) || 0) * soldQty;
      });
    } else if (tx.type === 'Продажа') {
      pCostFromRows[tx.plat] += (costBySku.get(normalizeArticle(tx.sku)) || 0) * (tx.qty || 0);
    } else if (tx.type === 'Возврат') {
      pCostFromRows[tx.plat] -= (costBySku.get(normalizeArticle(tx.sku)) || 0) * (tx.qty || 0);
    }
  });

  for (const plat of ['wb', 'ozon', 'ya']) {
    const pl = window.WATERFALL_BY_PLATFORM[plat];
    if (pl) pl[3].value = -Math.round(pCostFromRows[plat]);
  }

  // Рассчитаем предварительный налог по платформам (для базы "прибыль")
  const pTaxVal_pre_by_plat = {};
  for (const plat of ['wb', 'ozon', 'ya']) {
    if (activePlats.includes(plat)) {
      const pl = window.WATERFALL_BY_PLATFORM[plat];
      const pGross = pl ? pl[0].value : 0;
      const pMp = pl ? pl[1].value : 0;
      const pCost = pl ? -pl[3].value : 0;
      const taxPre = window.computeTaxForPlatform(
        { gross: pGross, mpAbs: -pMp, costAbs: pCost, opexAbs: pOpexVals[plat] },
        selectedIds, vatConfig, customConfig, splits[plat].tax
      ).total;
      pTaxVal_pre_by_plat[plat] = taxPre;
    } else {
      pTaxVal_pre_by_plat[plat] = 0;
    }
  }

  // Теперь считаем процентный опекс
  (window.OPEX_ROWS || []).forEach(r => {
    if (r.method !== 'percent') return;
    const pct = r.amount || 0;
    const base = r.percentBase || 'revenue';

    // Вспомогательная функция для расчета базы на платформе
    const getBaseForPlat = (plat) => {
      if (!activePlats.includes(plat)) return 0;
      const pl = window.WATERFALL_BY_PLATFORM[plat];
      if (!pl) return 0;
      const pGross = pl[0].value || 0;
      const pMp = pl[1].value || 0;
      if (base === 'revenue') {
        return pGross;
      } else if (base === 'transfer') {
        return Math.max(0, pGross + pMp);
      } else if (base === 'profit') {
        const pCost = pl[3].value || 0;
        const pTax = -Math.round(pTaxVal_pre_by_plat[plat] || 0);
        return Math.max(0, pGross + pMp + pCost + pTax);
      }
      return 0;
    };

    if (r.plat && r.plat !== 'all') {
      if (activePlats.includes(r.plat)) {
        const baseVal = getBaseForPlat(r.plat);
        const computed = Math.round(baseVal * pct / 100);
        pOpexVals[r.plat] += computed;
        totalOpexVal += computed;
        r.computedAmount = computed;
        r.computedAmountsByPlat[r.plat] = computed;
      } else {
        r.computedAmount = 0;
      }
    } else {
      let sumBase = 0;
      activePlats.forEach(p => {
        sumBase += getBaseForPlat(p);
      });
      const totalComputed = Math.round(sumBase * pct / 100);
      r.computedAmount = totalComputed;

      if (activePlats.includes('wb')) {
        const splitAmt = totalComputed * (splits.wb.opex / activeOpexSplitSum);
        pOpexVals.wb += splitAmt;
        r.computedAmountsByPlat.wb = splitAmt;
      }
      if (activePlats.includes('ozon')) {
        const splitAmt = totalComputed * (splits.ozon.opex / activeOpexSplitSum);
        pOpexVals.ozon += splitAmt;
        r.computedAmountsByPlat.ozon = splitAmt;
      }
      if (activePlats.includes('ya')) {
        const splitAmt = totalComputed * (splits.ya.opex / activeOpexSplitSum);
        pOpexVals.ya += splitAmt;
        r.computedAmountsByPlat.ya = splitAmt;
      }
      totalOpexVal += totalComputed;
    }
  });

  const opexVal = -Math.round(totalOpexVal);

  // Recalculate Waterfall BY PLATFORM first to get platform values
  for (const plat of ['wb', 'ozon', 'ya']) {
    const platList = window.WATERFALL_BY_PLATFORM[plat];
    if (!platList) continue;

    if (activePlats.includes(plat)) {
      const pOpexVal = -Math.round(pOpexVals[plat]);

      const pGross = platList[0].value;
      const pMp = platList[1].value;
      const pTransfer = pGross + pMp;
      const pCost = platList[3].value;
      const pTaxVal = -Math.round(window.computeTaxForPlatform(
        { gross: pGross, mpAbs: -pMp, costAbs: -pCost, opexAbs: pOpexVals[plat] },
        selectedIds, vatConfig, customConfig, splits[plat].tax
      ).total);
      taxVal += pTaxVal;
      const pProfit = pTransfer + pCost + pTaxVal + pOpexVal;

      platList[2].value = pTransfer;
      platList[4].value = pTaxVal;
      platList[4].hint = taxHint;
      platList[5].value = pOpexVal;
      platList[6].value = pProfit;
    } else {
      platList.forEach((wf) => {
        wf.value = 0;
      });
    }
  }

  // Now, calculate main Waterfall (all) as sum of active platforms
  let activeGross = 0;
  let activeMp = 0;
  let activeCost = 0;

  for (const plat of activePlats) {
    const platList = window.WATERFALL_BY_PLATFORM[plat];
    if (platList) {
      activeGross += platList[0].value;
      activeMp += platList[1].value;
      activeCost += platList[3].value;
    }
  }

  const gross = activeGross;
  const mp = activeMp;
  const transfer = gross + mp;
  const cost = activeCost;
  const profit = transfer + cost + taxVal + opexVal;

  window.WATERFALL[0].value = gross;
  window.WATERFALL[1].value = mp;
  window.WATERFALL[2].value = transfer;
  window.WATERFALL[3].value = cost;
  window.WATERFALL[4].value = taxVal;
  window.WATERFALL[4].hint = taxHint;
  window.WATERFALL[5].value = opexVal;
  window.WATERFALL[6].value = profit;

  // Recalculate DRR_BY_PLATFORM
  for (const plat of ['wb', 'ozon', 'ya']) {
    if (window.DRR_BY_PLATFORM && window.DRR_BY_PLATFORM[plat]) {
      const pDrr = window.DRR_BY_PLATFORM[plat];
      let adSpend = 0;
      viewTx.forEach(tx => {
        if (tx.plat === plat && tx.type === 'Отчет по начислениям') {
          adSpend += Math.abs(tx.report_meta?.store_expenses?.marketing || 0);
        }
      });
      (window.OPEX_ROWS || []).forEach(r => {
        if (r.plat === plat && r.name.toLowerCase().includes('реклама')) {
          adSpend += r.amount || 0;
        }
      });
      pDrr.ad_spend = Math.round(adSpend);
      const pGross = window.WATERFALL_BY_PLATFORM[plat] ? window.WATERFALL_BY_PLATFORM[plat][0].value : 0;
      pDrr.ad_revenue = pGross;
      pDrr.drr = pGross > 0 ? (adSpend / pGross) * 100 : 0;
    }
  }

  let activeAdSpend = 0;
  let activeAdRevenue = 0;
  let trendSum = [0, 0, 0, 0, 0];

  for (const plat of activePlats) {
    const pDrr = window.DRR_BY_PLATFORM[plat];
    if (pDrr) {
      activeAdSpend += pDrr.ad_spend;
      activeAdRevenue += pDrr.ad_revenue;
      for (let i = 0; i < 5; i++) {
        trendSum[i] += pDrr.trend[i] || 0;
      }
    }
  }

  if (window.DRR_BY_PLATFORM && window.DRR_BY_PLATFORM.all) {
    window.DRR_BY_PLATFORM.all.ad_spend = activeAdSpend;
    window.DRR_BY_PLATFORM.all.ad_revenue = activeAdRevenue;
    window.DRR_BY_PLATFORM.all.drr = activeAdRevenue > 0 ? (activeAdSpend / activeAdRevenue) * 100 : 0;
    window.DRR_BY_PLATFORM.all.trend = trendSum.map(val => activePlats.length > 0 ? val / activePlats.length : 0);
  }

  // Update PLATFORMS values and shares
  if (window.PLATFORMS) {
    window.PLATFORMS.forEach(p => {
      const platList = window.WATERFALL_BY_PLATFORM[p.id];
      p.value = platList ? platList[6].value : 0;
    });

    const totalActiveProfit = window.PLATFORMS
      .filter(p => activePlats.includes(p.id))
      .reduce((sum, p) => sum + p.value, 0);

    window.PLATFORMS.forEach(p => {
      p.share = totalActiveProfit > 0 ? Math.round((p.value / totalActiveProfit) * 1000) / 10 : 0;
    });
  }

  // Scale DAILY and REVENUE charts
  const initialTotalRevenue = 3420000;
  const initialTotalProfit = 487200;
  const revenueRatio = gross / initialTotalRevenue;
  const profitRatio = profit / initialTotalProfit;

  if (window.BASE_DAILY && window.DAILY) {
    window.DAILY.forEach((item, i) => {
      const baseItem = window.BASE_DAILY[i];
      if (baseItem) {
        item.v = Math.round(baseItem.v * profitRatio);
      }
    });
  }

  if (window.BASE_REVENUE && window.REVENUE) {
    window.REVENUE.forEach((val, i) => {
      const baseVal = window.BASE_REVENUE[i];
      if (baseVal !== undefined) {
        window.REVENUE[i] = Math.round(baseVal * revenueRatio);
      }
    });
  }

  // Update HERO
  if (window.HERO) {
    window.HERO.month.value = profit;
    window.HERO.custom.value = profit;
    window.HERO.day.value = Math.round(profit / 28);
    window.HERO.week.value = Math.round(profit / 4);
  }

  // Recalculate TOP_EXPENSES_BY_PLATFORM dynamically
  if (window.TOP_EXPENSES_BY_PLATFORM) {
    const expensesByPlat = {
      wb: {},
      ozon: {},
      ya: {},
      all: {}
    };

    const addExp = (plat, name, val) => {
      if (!val) return;
      const absVal = Math.abs(val);
      expensesByPlat[plat][name] = (expensesByPlat[plat][name] || 0) + absVal;
      expensesByPlat['all'][name] = (expensesByPlat['all'][name] || 0) + absVal;
    };

    viewTx.forEach(tx => {
      if (!activePlats.includes(tx.plat)) return;
      if (tx.type === 'Отчет по начислениям') {
        const exp = tx.report_meta?.store_expenses || {};
        if (exp.commission) addExp(tx.plat, 'Комиссия МП', exp.commission);
        if (exp.delivery) addExp(tx.plat, 'Логистика МП', exp.delivery);
        if (exp.marketing) addExp(tx.plat, 'Реклама', exp.marketing);
        if (exp.storage) addExp(tx.plat, 'Хранение', exp.storage);
        if (exp.returns) addExp(tx.plat, 'Возвраты МП', exp.returns);
        if (exp.other) addExp(tx.plat, 'Другие расходы МП', exp.other);
      } else if (MP_EXPENSE_TYPES.has(tx.type)) {
        let name = tx.type;
        if (tx.type === 'Комиссия') name = 'Комиссия МП';
        if (tx.type === 'Логистика') name = 'Логистика МП';
        if (tx.type === 'Хранение') name = 'Хранение';
        if (tx.type === 'Штраф') name = 'Штрафы';
        addExp(tx.plat, name, tx.amount);
      }
    });

    for (const plat of ['wb', 'ozon', 'ya']) {
      if (activePlats.includes(plat)) {
        const costVal = pCostFromRows[plat];
        if (costVal > 0) {
          addExp(plat, 'Себестоимость', costVal);
        }
      }
    }

    for (const plat of ['wb', 'ozon', 'ya']) {
      if (activePlats.includes(plat) && pOpexVals[plat] > 0) {
        addExp(plat, 'Операционные расходы', pOpexVals[plat]);
      }
    }

    for (const key of ['all', 'wb', 'ozon', 'ya']) {
      const list = [];
      const grossVal = key === 'all' ? gross : (window.WATERFALL_BY_PLATFORM[key] ? window.WATERFALL_BY_PLATFORM[key][0].value : 0);
      
      Object.entries(expensesByPlat[key]).forEach(([name, val]) => {
        list.push({
          name,
          value: Math.round(val),
          pct: grossVal > 0 ? Math.round((val / grossVal) * 1000) / 10 : 0
        });
      });
      
      list.sort((a, b) => b.value - a.value);
      window.TOP_EXPENSES_BY_PLATFORM[key] = list.slice(0, 5);
    }
  }

  // Recalculate ACCURACY dynamically
  if (window.ACCURACY) {
    let confirmed = 0;
    let pending = 0;

    viewTx.forEach(tx => {
      if (!activePlats.includes(tx.plat)) return;
      if (tx.type === 'Отчет по начислениям') {
        confirmed += tx.gross_revenue || tx.amount || 0;
      } else if (tx.type === 'Продажа' || tx.type === 'Возврат') {
        pending += tx.amount || 0;
      }
    });

    window.ACCURACY.confirmed = Math.round(confirmed);
    window.ACCURACY.pending = Math.round(pending);
    const totalAcc = confirmed + pending;
    window.ACCURACY.share_confirmed = totalAcc > 0 ? Math.round((confirmed / totalAcc) * 1000) / 10 : 0;
    
    window.ACCURACY.items[0].value = Math.round(confirmed);
    window.ACCURACY.items[0].note = confirmed > 0 ? 'Из загруженных финансовых отчётов' : 'Нет загруженных отчётов';
    
    window.ACCURACY.items[1].value = Math.round(pending);
    window.ACCURACY.items[1].note = pending > 0 ? 'Текущие продажи за незакрытый период' : 'Нет текущих транзакций';
  }

  // Recalculate RECONCILE dynamically
  if (window.RECONCILE) {
    window.RECONCILE.transfer = transfer;
    window.RECONCILE.cost = Math.round(pCostFromRows.wb + pCostFromRows.ozon + pCostFromRows.ya);
    window.RECONCILE.margin_after_cost = transfer - window.RECONCILE.cost;
    const ratio = window.RECONCILE.cost > 0 ? transfer / window.RECONCILE.cost : 0;
    window.RECONCILE.ratio = Math.round(ratio * 100) / 100;
    window.RECONCILE.status = ratio >= 1.5 ? 'ok' : ratio >= 1.2 ? 'warn' : 'danger';
  }

  // Recalculate ALL_PRODUCTS dynamically
  if (window.ALL_PRODUCTS) {
    // Доля выплаты в выручке по площадке (из «Отчетов по начислениям») — чтобы
    // оценивать net по артикулам WB из «Отчета по продажам», где net_payout нет.
    const payoutRatio = {};
    viewTx.forEach(tx => {
      if (!activePlats.includes(tx.plat)) return;
      if (tx.type !== 'Отчет по начислениям') return;
      const r = payoutRatio[tx.plat] = payoutRatio[tx.plat] || { net: 0, gross: 0 };
      r.net += tx.amount || 0;
      r.gross += tx.gross_revenue || tx.amount || 0;
    });

    const productsMap = new Map();
    viewTx.forEach(tx => {
      if (!activePlats.includes(tx.plat)) return;
      if ((tx.type === 'Отчет по начислениям' || tx.type === 'Отчет по продажам')
          && Array.isArray(tx.skus) && tx.skus.length) {
        tx.skus.forEach(s => {
          const article = s.article || s.sku_id;
          if (!article) return;
          const key = normalizeArticle(article);
          if (!productsMap.has(key)) {
            productsMap.set(key, {
              sku: article,
              name: s.name || article,
              category: '—',
              subcat: '—',
              cost: costBySku.get(key) || 0,
              price: 0,
              wb_units: 0,
              ozon_units: 0,
              ya_units: 0,
              total_units: 0,
              gross: 0,
              net: 0,
              margin: 0,
              marginWas: 0,
              priority: null,
              comment: '',
              platAgg: {},
              platforms: []
            });
          }
          const p = productsMap.get(key);
          const soldUnits = Math.max(0, (s.sales_count || 0) - (s.returns_count || 0));
          if (tx.plat === 'wb') p.wb_units += soldUnits;
          else if (tx.plat === 'ozon') p.ozon_units += soldUnits;
          else if (tx.plat === 'ya') p.ya_units += soldUnits;

          const rev = s.gross_revenue || 0;
          // WB в «Отчете по продажам» не даёт net_payout по артикулу — оцениваем
          // пропорционально доле «к перечислению» площадки из отчёта начислений.
          const r = payoutRatio[tx.plat];
          const net = s.net_payout != null ? s.net_payout
            : rev * (r && r.gross > 0 ? r.net / r.gross : 1);

          p.total_units += soldUnits;
          p.gross += rev;
          p.net += net;

          const agg = p.platAgg[tx.plat] = p.platAgg[tx.plat] || { rev: 0, net: 0, units: 0 };
          agg.rev += rev;
          agg.net += net;
          agg.units += soldUnits;
        });
      }
    });

    const list = Array.from(productsMap.values()).map(p => {
      p.price = p.total_units > 0 ? Math.round(p.gross / p.total_units) : 0;
      const costTotal = p.cost * p.total_units;
      const skuProfit = p.net - costTotal;
      p.margin = p.gross > 0 ? Math.round((skuProfit / p.gross) * 100) : 0;
      p.marginWas = p.margin;
      p.gross = Math.round(p.gross);
      p.net = Math.round(p.net);
      p.rev = p.gross;
      p.units = p.total_units;

      ['wb', 'ozon', 'ya'].forEach(plat => {
        const agg = p.platAgg[plat];
        if (agg && (agg.rev > 0 || agg.units > 0)) {
          const platProfit = agg.net - p.cost * agg.units;
          const platMargin = agg.rev > 0 ? Math.round((platProfit / agg.rev) * 100) : 0;
          p.platforms.push({ plat, rev: Math.round(agg.rev), units: agg.units, margin: platMargin });
        }
      });
      delete p.platAgg;
      return p;
    });

    window.ALL_PRODUCTS.length = 0;
    list.forEach(p => window.ALL_PRODUCTS.push(p));
  }

  // Recalculate SALES_BY_PERIOD dynamically
  if (window.SALES_BY_PERIOD) {
    for (const plat of ['wb', 'ozon', 'ya']) {
      for (const pId of ['month', 'custom']) {
        const s = window.SALES_BY_PERIOD[pId][plat];
        if (s) {
          s.total = pGrossFromTx[plat];
          s.units = pUnitsFromTx[plat];
          s.delta = 0;
          
          if (pGrossFromTx[plat] > 0) {
            const daysCount = 28;
            const avgVal = pGrossFromTx[plat] / daysCount;
            s.series = _days28.map((d, i) => ({
              v: avgVal * (BASE_DAILY[i % BASE_DAILY.length].v / 25000),
              d
            }));
          } else {
            s.series = _days28.map(d => ({ v: 0, d }));
          }
        }
      }
      
      const sd = window.SALES_BY_PERIOD.day[plat];
      if (sd) {
        sd.total = Math.round(pGrossFromTx[plat] / 28);
        sd.units = Math.round(pUnitsFromTx[plat] / 28);
        sd.delta = 0;
        sd.series = Array.from({ length: 7 }, (_, i) => ({ v: sd.total / 7, d: `Day ${i}` }));
      }
      
      const sw = window.SALES_BY_PERIOD.week[plat];
      if (sw) {
        sw.total = Math.round(pGrossFromTx[plat] / 4);
        sw.units = Math.round(pUnitsFromTx[plat] / 4);
        sw.delta = 0;
        sw.series = Array.from({ length: 7 }, (_, i) => ({ v: sw.total / 7, d: `Day ${i}` }));
      }
    }
  }

  recalculateTopSku();
};

// Живая проекция TAX_REGIMES: baseAmount/tax для УСН-6/15 из уже посчитанной
// window.WATERFALL (не зависит от текущего TAX_SELECTED — можно сравнивать
// режимы, не переключая их); Патент/ОСНО берут сумму из window.MANUAL_TAX.
window.getLiveTaxRegimes = () => {
  const gross = window.WATERFALL?.[0]?.value || 0;
  const mpAbs = Math.abs(window.WATERFALL?.[1]?.value || 0);
  const costAbs = Math.abs(window.WATERFALL?.[3]?.value || 0);
  const opexAbs = Math.abs(window.WATERFALL?.[5]?.value || 0);
  const manual = window.MANUAL_TAX || {};

  return (window.TAX_REGIMES || TAX_REGIMES).map(t => {
    if (t.id === 'usn6') {
      return { ...t, baseAmount: gross, tax: gross * 0.06 };
    }
    if (t.id === 'usn15') {
      const base = gross - mpAbs - costAbs - opexAbs;
      const baseAmount = base > 0 ? base : 0;
      return { ...t, baseAmount, tax: baseAmount * 0.15 };
    }
    const manualVal = manual[t.id];
    return { ...t, baseAmount: null, tax: manualVal != null ? manualVal : 0 };
  });
};

// ── Себестоимость: слияние импорта и валидация формы (общие для десктопа и мобайла) ──
// Импорт из Excel побеждает при совпадении SKU — перезаписывает name/cost/upd и ставит src:'excel',
// даже если строка была отредактирована вручную. SKU, не встретившиеся в файле, не трогаются.
window.mergeCostRows = (existingRows, importedRows) => {
  const bySku = new Map((existingRows || []).map(r => [r.sku, r]));
  (importedRows || []).forEach(imp => {
    const prev = bySku.get(imp.sku);
    bySku.set(imp.sku, {
      sku: imp.sku,
      name: imp.name || prev?.name || '',
      cost: imp.cost,
      upd: imp.date,
      src: 'excel'
    });
  });
  return [...bySku.values()];
};

window.validateCostForm = (rows, form, editingSku) => {
  const errors = {};
  const sku = (form.sku || '').trim();
  if (!sku) {
    errors.sku = 'Введите SKU';
  } else if (sku !== editingSku && (rows || []).some(r => r.sku === sku)) {
    errors.sku = 'Такой SKU уже есть в списке';
  }
  if (!(form.name || '').trim()) errors.name = 'Введите название товара';
  const cost = parseFloat(String(form.cost).replace(',', '.'));
  if (form.cost === '' || form.cost == null || isNaN(cost) || cost < 0) {
    errors.cost = 'Введите себестоимость (0 или больше)';
  }
  return errors;
};

// Database Save Helper
window.saveCompanyDataToServer = async () => {
  const coId = window.STORE?.id;
  if (!coId) return;
  try {
    await fetch(`/api/company-data/${coId}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-User-Token': localStorage.getItem('cp-user-token') || ''
      },
      body: JSON.stringify({
        cabinets: window.CABINETS,
        cost_rows: window.COST_ROWS,
        tax_selected: window.TAX_SELECTED,
        opex_rows: window.OPEX_ROWS,
        raw_tx: window.RAW_TX,
        active_marketplaces: window.ACTIVE_MARKETPLACES,
        manual_tax: window.MANUAL_TAX,
        vat_config: window.VAT_CONFIG,
        custom_tax_config: window.CUSTOM_TAX_CONFIG
      })
    });
    window.dispatchEvent(new CustomEvent('raw_tx_updated'));
  } catch (err) {
    console.error('Error saving company data to database:', err);
  }
};

