const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const { Pool } = require('pg');
const { initializeDatabase } = require('./dbInit');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { spawn } = require('child_process');
const { v4: uuidv4 } = require('uuid');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const FileProcessor = require('./fileProcessor');
const BalanceSheetProcessor = require('./balanceSheetProcessor');
const BalanceSheet76Processor = require('./balanceSheet76Processor');
const DebtorReportProcessor = require('./debtorReportProcessor');
const PipelineAutomation = require('./pipelineAutomation');
const paymentInvoiceWorkflow = require('./paymentInvoiceWorkflow');
const notificationService = require('./notificationService');
const { ROLE_ACCESS, SECTION_IDS } = require('./constants/roleAccess');
const axios = require('axios');
const aiChatService = require('./aiChatService');

dotenv.config();

const app = express();
app.use(cors());
app.use(express.json());

// Заголовки безопасности (E3: защита от XSS, clickjacking)
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  next();
});

const PORT = process.env.PORT || 4000;
const API_PREFIX = '/api';
const isProduction = process.env.NODE_ENV === 'production';
const JWT_SECRET = process.env.JWT_SECRET || (isProduction ? null : 'mkd-control-center-secret-change-in-production');
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';

if (isProduction && !process.env.JWT_SECRET) {
  console.error('[backend] В production необходимо задать переменную окружения JWT_SECRET. Сервер не запущен.');
  process.exit(1);
}

// Строка подключения к PostgreSQL.
// Пример: postgres://user:password@localhost:5432/mkd_control_center
const DATABASE_URL = process.env.DATABASE_URL;
const DOMA_API_URL = process.env.DOMA_API_URL;
const DOMA_API_TOKEN = process.env.DOMA_API_TOKEN;

if (!DATABASE_URL) {
  console.warn(
    '[backend] Внимание: переменная окружения DATABASE_URL не задана. ' +
      'Бэкенд не сможет подключиться к PostgreSQL, пока вы её не настроите.'
  );
}

const pool = new Pool({
  connectionString: DATABASE_URL,
});

// Инициализация обработчиков файлов
const fileProcessor = new FileProcessor(pool);
const balanceSheetProcessor = new BalanceSheetProcessor(pool);
const balanceSheet76Processor = new BalanceSheet76Processor();
const debtorReportProcessor = new DebtorReportProcessor(pool);
const pipelineAutomation = new PipelineAutomation(pool);

// Настройка multer для загрузки файлов
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir, { recursive: true });
}

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, uploadDir);
  },
  filename: (req, file, cb) => {
    const uniqueName = `${uuidv4()}-${file.originalname}`;
    cb(null, uniqueName);
  }
});

const upload = multer({
  storage,
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['.csv', '.xlsx', '.xls'];
    const ext = path.extname(file.originalname).toLowerCase();
    if (allowedTypes.includes(ext)) {
      cb(null, true);
    } else {
      cb(new Error('Неподдерживаемый тип файла. Разрешены только CSV и XLSX'));
    }
  },
  limits: {
    fileSize: 50 * 1024 * 1024 // 50MB
  }
});

// Настройка multer для загрузки изображений (фото сотрудников)
const photosDir = path.join(__dirname, 'uploads', 'photos');
if (!fs.existsSync(photosDir)) {
  fs.mkdirSync(photosDir, { recursive: true });
}

const photoStorage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, photosDir);
  },
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname).toLowerCase();
    const uniqueName = `${uuidv4()}${ext}`;
    cb(null, uniqueName);
  }
});

const uploadPhoto = multer({
  storage: photoStorage,
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
    const ext = path.extname(file.originalname).toLowerCase();
    if (allowedTypes.includes(ext)) {
      cb(null, true);
    } else {
      cb(new Error('Неподдерживаемый тип файла. Разрешены только изображения (JPG, PNG, GIF, WEBP)'));
    }
  },
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB
  }
});

// Настройка multer для загрузки файлов базы знаний
const knowledgeBaseDir = path.join(__dirname, 'uploads', 'knowledge-base');
if (!fs.existsSync(knowledgeBaseDir)) {
  fs.mkdirSync(knowledgeBaseDir, { recursive: true });
}

const knowledgeBaseStorage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, knowledgeBaseDir);
  },
  filename: (req, file, cb) => {
    const uniqueName = `${uuidv4()}-${file.originalname}`;
    cb(null, uniqueName);
  }
});

const uploadKnowledgeBase = multer({
  storage: knowledgeBaseStorage,
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf', '.doc', '.docx', '.txt', '.zip', '.rar'];
    const ext = path.extname(file.originalname).toLowerCase();
    if (allowedTypes.includes(ext)) {
      cb(null, true);
    } else {
      cb(new Error('Неподдерживаемый тип файла'));
    }
  },
  limits: {
    fileSize: 20 * 1024 * 1024 // 20MB
  }
});

// Настройка multer для загрузки файлов документов корреспонденции
const documentsDir = path.join(__dirname, 'uploads', 'documents');
if (!fs.existsSync(documentsDir)) {
  fs.mkdirSync(documentsDir, { recursive: true });
}

const documentsStorage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, documentsDir);
  },
  filename: (req, file, cb) => {
    // Исправляем кодировку имени файла
    let originalName = file.originalname;
    try {
      // Пробуем декодировать из latin1 в utf8, если имя содержит кракозябры
      const hasCyrillic = /[А-Яа-яЁё]/.test(originalName);
      const hasGarbled = /[ÐÑ]/i.test(originalName) || /[â€]/i.test(originalName);
      
      if (hasGarbled || (!hasCyrillic && /[^\x00-\x7F]/.test(originalName))) {
        try {
          const buffer = Buffer.from(originalName, 'latin1');
          const decoded = buffer.toString('utf8');
          if (/[А-Яа-яЁё]/.test(decoded)) {
            originalName = decoded;
          }
        } catch (decodeErr) {
          try {
            originalName = decodeURIComponent(escape(originalName));
          } catch (e) {
            console.warn('Не удалось исправить кодировку имени файла');
          }
        }
      }
    } catch (decodeErr) {
      console.warn('Ошибка декодирования имени файла, используем оригинал:', decodeErr);
    }
    
    // Сохраняем оригинальное имя в безопасном формате
    const ext = path.extname(originalName);
    const baseName = path.basename(originalName, ext);
    // Транслитерируем кириллицу для имени файла или используем только UUID
    const safeName = baseName.replace(/[^a-zA-Z0-9_-]/g, '_').substring(0, 50);
    const uniqueName = `${uuidv4()}-${safeName}${ext}`;
    cb(null, uniqueName);
  }
});

const uploadDocuments = multer({
  storage: documentsStorage,
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf', '.doc', '.docx', '.txt'];
    const ext = path.extname(file.originalname).toLowerCase();
    if (allowedTypes.includes(ext)) {
      cb(null, true);
    } else {
      cb(new Error('Неподдерживаемый тип файла. Разрешены: изображения, PDF, DOC, DOCX, TXT'));
    }
  },
  limits: {
    fileSize: 20 * 1024 * 1024 // 20MB
  }
});

// Настройка multer для типовых документов HR (кадры)
const hrTemplatesDir = path.join(__dirname, 'uploads', 'hr-templates');
if (!fs.existsSync(hrTemplatesDir)) {
  fs.mkdirSync(hrTemplatesDir, { recursive: true });
}
const hrTemplatesStorage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, hrTemplatesDir),
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname).toLowerCase() || '';
    cb(null, `${uuidv4()}${ext}`);
  }
});
const uploadHrTemplate = multer({
  storage: hrTemplatesStorage,
  fileFilter: (req, file, cb) => {
    const allowed = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt', '.odt'];
    const ext = path.extname(file.originalname).toLowerCase();
    if (allowed.includes(ext)) {
      cb(null, true);
    } else {
      cb(new Error('Разрешены: PDF, DOC, DOCX, XLS, XLSX, TXT, ODT'));
    }
  },
  limits: { fileSize: 15 * 1024 * 1024 }
});

// Настройка multer для загрузки файлов счетов на оплату
const invoicesDir = path.join(__dirname, 'uploads', 'payment-invoices');
if (!fs.existsSync(invoicesDir)) {
  fs.mkdirSync(invoicesDir, { recursive: true });
}

const invoicesStorage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, invoicesDir);
  },
  filename: (req, file, cb) => {
    // Исправляем кодировку имени файла
    let originalName = file.originalname;
    try {
      const hasCyrillic = /[А-Яа-яЁё]/.test(originalName);
      const hasGarbled = /[ÐÑ]/i.test(originalName) || /[â€]/i.test(originalName);
      
      if (hasGarbled || (!hasCyrillic && /[^\x00-\x7F]/.test(originalName))) {
        try {
          const buffer = Buffer.from(originalName, 'latin1');
          const decoded = buffer.toString('utf8');
          if (/[А-Яа-яЁё]/.test(decoded)) {
            originalName = decoded;
          }
        } catch (decodeErr) {
          try {
            originalName = decodeURIComponent(escape(originalName));
          } catch (e) {
            console.warn('Не удалось исправить кодировку имени файла');
          }
        }
      }
    } catch (decodeErr) {
      console.warn('Ошибка декодирования имени файла, используем оригинал:', decodeErr);
    }
    
    const ext = path.extname(originalName);
    const baseName = path.basename(originalName, ext);
    const safeName = baseName.replace(/[^a-zA-Z0-9_-]/g, '_').substring(0, 50);
    const uniqueName = `${uuidv4()}-${safeName}${ext}`;
    cb(null, uniqueName);
  }
});

const uploadInvoice = multer({
  storage: invoicesStorage,
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt', '.zip', '.rar'];
    const ext = path.extname(file.originalname).toLowerCase();
    if (allowedTypes.includes(ext)) {
      cb(null, true);
    } else {
      cb(new Error('Неподдерживаемый тип файла. Разрешены: изображения, PDF, DOC, DOCX, XLS, XLSX, TXT, ZIP, RAR'));
    }
  },
  limits: {
    fileSize: 50 * 1024 * 1024 // 50MB
  }
});

// Настройка multer для загрузки файлов договоров (юридический отдел)
const contractsDir = path.join(__dirname, 'uploads', 'contracts');
if (!fs.existsSync(contractsDir)) {
  fs.mkdirSync(contractsDir, { recursive: true });
}

const contractsStorage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, contractsDir),
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname).toLowerCase() || '.bin';
    const safeName = path.basename(file.originalname, ext).replace(/[^a-zA-Z0-9_-]/g, '_').substring(0, 50);
    cb(null, `${uuidv4()}-${safeName}${ext}`);
  }
});

const uploadContract = multer({
  storage: contractsStorage,
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.jpg', '.jpeg', '.png', '.txt', '.zip', '.rar'];
    const ext = path.extname(file.originalname).toLowerCase();
    if (allowedTypes.includes(ext)) {
      cb(null, true);
    } else {
      cb(new Error('Неподдерживаемый тип файла. Разрешены: PDF, DOC, DOCX, XLS, XLSX, изображения, TXT, ZIP, RAR'));
    }
  },
  limits: { fileSize: 50 * 1024 * 1024 }
});

// Статическая раздача загруженных файлов
app.use('/uploads', express.static(uploadDir));

// Инициализация БД и запуск сервера — listen только после готовности БД
async function startServer() {
  if (DATABASE_URL) {
    try {
      const initResult = await initializeDatabase();
      if (initResult.success) {
        console.log('[backend] База данных инициализирована успешно');
        if (initResult.initialized) {
          console.log('[backend] База данных была создана и настроена');
        }
      } else {
        console.error('[backend] Ошибка инициализации БД:', initResult.reason || initResult.error);
        process.exit(1);
      }
      // Таблица employee_districts создаётся до listen, чтобы быть готовой к первым запросам
      try {
        await ensureEmployeeDistrictsTable();
        console.log('[backend] Таблица employee_districts проверена/создана');
      } catch (err) {
        console.warn('[backend] employee_districts при старте:', err.message);
      }
    } catch (error) {
      console.error('[backend] Критическая ошибка при инициализации БД:', error);
      process.exit(1);
    }
  }
  app.listen(PORT, '0.0.0.0', () => {
    console.log(`Backend API running on http://0.0.0.0:${PORT}${API_PREFIX}`);
    if (DATABASE_URL) {
      ensureEmployeeResponsibilityTable()
        .then(() => console.log('[backend] Таблица employee_responsibility проверена/создана'))
        .catch((err) => console.warn('[backend] employee_responsibility:', err.message));
      ensureSickLeaveExpectedReturnDateColumn()
        .then(() => console.log('[backend] Колонка employee_sick_leaves.expected_return_date проверена'))
        .catch((err) => console.warn('[backend] employee_sick_leaves.expected_return_date:', err.message));
      startDailyPipelineCheck();
      console.log('[Cron] Pipeline automation scheduler started');
      console.log('[Auto Reports] Автоматическое создание отчетов включено (1 число каждого месяца)');
    }
  });
}

// Хелпер для запросов к БД
async function query(text, params) {
  const client = await pool.connect();
  try {
    const res = await client.query(text, params);
    return res.rows;
  } finally {
    client.release();
  }
}

/** Применяет миграцию report_type: добавляет balance_sheet_76 в CHECK (для ОСВ 76 без перезапуска). */
async function ensureBalanceSheet76ReportType() {
  const client = await pool.connect();
  try {
    await client.query(`ALTER TABLE financial_reports DROP CONSTRAINT IF EXISTS financial_reports_report_type_check`);
    const { rows } = await client.query(`
      SELECT c.conname FROM pg_constraint c
      JOIN pg_class t ON t.oid = c.conrelid
      JOIN pg_namespace n ON n.oid = t.relnamespace
      WHERE n.nspname = 'public' AND t.relname = 'financial_reports' AND c.contype = 'c'
      AND (pg_get_constraintdef(c.oid) LIKE '%report_type%' OR c.conname LIKE '%report_type%')
    `);
    for (const row of rows) {
      const name = String(row.conname).replace(/"/g, '""');
      await client.query(`ALTER TABLE financial_reports DROP CONSTRAINT IF EXISTS "${name}"`);
    }
    await client.query(`
      ALTER TABLE financial_reports ADD CONSTRAINT financial_reports_report_type_check
        CHECK (report_type IN ('debtors', 'balance_sheet', 'balance_sheet_76', 'other'))
    `);
  } finally {
    client.release();
  }
}

/** Создаёт таблицы report_76_rows и building_personal_account_mappings, если их ещё нет (для ОСВ 76 без перезапуска). */
async function ensureReport76Tables() {
  const exists = await query(
    `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'report_76_rows'`
  );
  if (exists.length > 0) return;
  const client = await pool.connect();
  try {
    await client.query(`
      CREATE TABLE IF NOT EXISTS report_76_rows (
        id                BIGSERIAL PRIMARY KEY,
        report_id         BIGINT NOT NULL REFERENCES financial_reports(id) ON DELETE CASCADE,
        row_index         INTEGER NOT NULL,
        account_label     TEXT NOT NULL,
        account_ls        TEXT,
        saldo_start_debet NUMERIC(15, 2) DEFAULT 0,
        turnover_debet    NUMERIC(15, 2) DEFAULT 0,
        turnover_credit   NUMERIC(15, 2) DEFAULT 0,
        saldo_end_debet   NUMERIC(15, 2) DEFAULT 0,
        saldo_end_credit  NUMERIC(15, 2) DEFAULT 0,
        created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
      )
    `);
    await client.query(`CREATE INDEX IF NOT EXISTS idx_report_76_rows_report_id ON report_76_rows(report_id)`);
    await client.query(`
      CREATE TABLE IF NOT EXISTS building_personal_account_mappings (
        id            BIGSERIAL PRIMARY KEY,
        building_id   VARCHAR(50) NOT NULL REFERENCES buildings(id) ON DELETE CASCADE,
        account_ls    TEXT NOT NULL,
        account_label TEXT,
        apartment     TEXT,
        created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        updated_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        UNIQUE(building_id, account_ls)
      )
    `);
    await client.query(`CREATE INDEX IF NOT EXISTS idx_building_personal_account_mappings_building ON building_personal_account_mappings(building_id)`);
    await client.query(`CREATE INDEX IF NOT EXISTS idx_building_personal_account_mappings_account_ls ON building_personal_account_mappings(account_ls)`);
  } finally {
    client.release();
  }
}

/** Проверяет/создаёт таблицу employee_responsibility (зоны ответственности сотрудников). */
async function ensureEmployeeResponsibilityTable() {
  const rows = await query(
    `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'employee_responsibility'`
  );
  if (rows.length > 0) return;
  await query(`
    CREATE TABLE employee_responsibility (
      id          BIGSERIAL PRIMARY KEY,
      employee_id VARCHAR(50) NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
      section     VARCHAR(50) NOT NULL,
      sub_id      VARCHAR(50) NOT NULL,
      created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      UNIQUE(employee_id, section, sub_id)
    )
  `);
  await query(`CREATE INDEX IF NOT EXISTS idx_employee_responsibility_employee ON employee_responsibility(employee_id)`);
  await query(`CREATE INDEX IF NOT EXISTS idx_employee_responsibility_zone ON employee_responsibility(section, sub_id)`);
}

/** Добавляет колонку expected_return_date в employee_sick_leaves, если её нет (предварительная дата выхода). */
async function ensureSickLeaveExpectedReturnDateColumn() {
  const rows = await query(
    `SELECT column_name FROM information_schema.columns 
     WHERE table_schema = 'public' AND table_name = 'employee_sick_leaves' AND column_name = 'expected_return_date'`
  );
  if (rows.length > 0) return;
  await query(`ALTER TABLE employee_sick_leaves ADD COLUMN IF NOT EXISTS expected_return_date DATE`);
}

/** Создаёт таблицу employee_districts (связь сотрудник — участки, многие ко многим), если её нет. */
async function ensureEmployeeDistrictsTable() {
  const rows = await query(
    `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'employee_districts'`
  );
  if (rows.length > 0) {
    return;
  }
  console.log('[backend] Создаём таблицу employee_districts...');
  await query(`
    CREATE TABLE employee_districts (
      employee_id VARCHAR(50) NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
      district_id VARCHAR(50) NOT NULL REFERENCES districts(id) ON DELETE CASCADE,
      created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      PRIMARY KEY (employee_id, district_id)
    )
  `);
  await query(`CREATE INDEX IF NOT EXISTS idx_employee_districts_employee ON employee_districts(employee_id)`);
  await query(`CREATE INDEX IF NOT EXISTS idx_employee_districts_district ON employee_districts(district_id)`);
  try {
    await query(`
      INSERT INTO employee_districts (employee_id, district_id)
      SELECT id, assigned_district_id
      FROM employees
      WHERE assigned_district_id IS NOT NULL AND assigned_district_id <> ''
        AND assigned_district_id IN (SELECT id FROM districts)
      ON CONFLICT (employee_id, district_id) DO NOTHING
    `);
  } catch (insertErr) {
    console.warn('[backend] employee_districts: миграция данных пропущена:', insertErr.message);
  }
}

/**
 * Перенос ответственности с уволенного сотрудника на его руководителя.
 * Вызывается при создании увольнения (employees.status = 'inactive').
 */
async function transferResponsibilityToManager(employeeId) {
  try {
    const empRows = await query(
      'SELECT id, name, manager_id FROM employees WHERE id = $1',
      [employeeId]
    );
    if (empRows.length === 0) return;
    const employeeName = empRows[0].name;
    const managerId = empRows[0].manager_id;
    if (!managerId) {
      console.warn(`[transferResponsibility] У сотрудника ${employeeId} (${employeeName}) нет руководителя (manager_id). Перенос ответственности пропущен.`);
      return;
    }
    const managerRows = await query('SELECT name FROM employees WHERE id = $1', [managerId]);
    if (managerRows.length === 0) {
      console.warn(`[transferResponsibility] Руководитель ${managerId} не найден. Перенос ответственности пропущен.`);
      return;
    }
    const managerName = managerRows[0].name;

    await query(
      `UPDATE applications SET executor_name = $1, responsible_name = $1 
       WHERE executor_name = $2 OR responsible_name = $2`,
      [managerName, employeeName]
    );
    await query('UPDATE incidents SET assigned_to = $1 WHERE assigned_to = $2', [managerName, employeeName]);
    await query('UPDATE pre_trial_work SET assigned_to = $1 WHERE assigned_to = $2', [managerName, employeeName]);
    await query('UPDATE office_equipment SET assigned_to = $1 WHERE assigned_to = $2', [managerName, employeeName]);
    await query('UPDATE office_documents SET assigned_to = $1 WHERE assigned_to = $2', [managerName, employeeName]);
    await query('UPDATE pr_event_assignees SET employee_id = $1 WHERE employee_id = $2', [managerId, employeeId]);

    console.log(`[transferResponsibility] Ответственность сотрудника ${employeeId} (${employeeName}) перенесена на руководителя ${managerId} (${managerName}).`);
  } catch (err) {
    console.error('[transferResponsibility] Ошибка переноса ответственности:', err);
  }
}

// ----------- RATE LIMIT ДЛЯ ВХОДА (E1: защита от брутфорса) -----------
const LOGIN_RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 минут
const LOGIN_RATE_LIMIT_MAX_ATTEMPTS = 10;
const loginAttempts = new Map(); // key: IP (string), value: { count, resetAt }

function getClientIp(req) {
  return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket?.remoteAddress || 'unknown';
}

function loginRateLimitMiddleware(req, res, next) {
  if (req.method !== 'POST' || req.path !== '/auth/login') return next();
  const ip = getClientIp(req);
  const now = Date.now();
  const rec = loginAttempts.get(ip);
  if (rec && now < rec.resetAt && rec.count >= LOGIN_RATE_LIMIT_MAX_ATTEMPTS) {
    return res.status(429).json({ error: 'Слишком много неудачных попыток входа. Попробуйте позже.' });
  }
  if (rec && now >= rec.resetAt) {
    loginAttempts.delete(ip);
  }
  req._loginRateLimitIp = ip;
  next();
}

function recordLoginFailure(ip) {
  const now = Date.now();
  const rec = loginAttempts.get(ip) || { count: 0, resetAt: now + LOGIN_RATE_LIMIT_WINDOW_MS };
  if (now >= rec.resetAt) {
    rec.count = 0;
    rec.resetAt = now + LOGIN_RATE_LIMIT_WINDOW_MS;
  }
  rec.count++;
  loginAttempts.set(ip, rec);
}

function clearLoginAttempts(ip) {
  loginAttempts.delete(ip);
}

function maskLogin(login) {
  if (!login || typeof login !== 'string') return '***';
  const s = String(login).trim();
  if (s.length <= 2) return '***';
  return s.substring(0, 2) + '***';
}

async function isBlacklisted(ip, login) {
  try {
    const ipRows = await query('SELECT 1 FROM security_blacklist WHERE type = $1 AND LOWER(TRIM(value)) = LOWER(TRIM($2))', ['ip', ip]);
    if (ipRows.length > 0) return true;
    const loginStr = login && String(login).trim();
    if (loginStr) {
      const loginRows = await query('SELECT 1 FROM security_blacklist WHERE type = $1 AND LOWER(TRIM(value)) = LOWER(TRIM($2))', ['login', loginStr]);
      if (loginRows.length > 0) return true;
    }
  } catch (e) {
    // если таблицы ещё нет — не блокируем
  }
  return false;
}

async function writeAuthLog(ip, loginMasked, success) {
  try {
    await query('INSERT INTO auth_logs (ip, login_masked, success) VALUES ($1, $2, $3)', [ip, loginMasked, success]);
  } catch (e) {
    // игнорируем ошибки записи лога (таблица может отсутствовать)
  }
}

// ----------- АВТОРИЗАЦИЯ -----------
// ROLE_ACCESS и SECTION_IDS из единого источника (constants/roleAccess.js)
function requireAuth(req, res, next) {
  const authHeader = req.headers.authorization;
  const token = authHeader && authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
  if (!token) {
    return res.status(401).json({ error: 'Требуется авторизация' });
  }
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    // Гость без учётки — доступ только к публичным отчётам и NPS-опросам
    if (decoded.role === 'GUEST') {
      const reportOk = req.method === 'GET' && /^\/pr\/reports\/[^/]+(\/data)?$/.test(req.path);
      const npsGetOk = req.method === 'GET' && /^\/pr\/nps-surveys\/[^/]+$/.test(req.path);
      const npsPostOk = req.method === 'POST' && /^\/pr\/nps-surveys\/[^/]+\/responses$/.test(req.path);
      const allowed = reportOk || npsGetOk || npsPostOk;
      if (!allowed) {
        return res.status(403).json({ error: 'Доступ запрещён для гостя' });
      }
    }
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Недействительный или истёкший токен' });
  }
}

function authMiddleware(req, res, next) {
  if (req.method === 'POST' && req.path === '/auth/login') return next();
  if (req.method === 'GET' && (req.path === '/health/db' || req.path === '/health')) return next();
  // Файлы фото мероприятий — для отображения в img (без auth в заголовках)
  if (req.method === 'GET' && /^\/pr\/events\/\d+\/photos\/file\/[^/]+$/.test(req.path)) return next();
  // Синхронизация Doma: без авторизации, если передан секрет (для cron). Секрет задаётся в DOMA_SYNC_SECRET.
  const domaSyncSecret = process.env.DOMA_SYNC_SECRET;
  if ((req.path === '/doma/sync-now') && domaSyncSecret && req.query && req.query.secret === domaSyncSecret) return next();
  requireAuth(req, res, next);
}

app.use(API_PREFIX, loginRateLimitMiddleware);

// POST /api/auth/login — без auth
app.post(`${API_PREFIX}/auth/login`, async (req, res) => {
  const rateLimitIp = req._loginRateLimitIp;
  const clientIp = rateLimitIp || getClientIp(req);
  try {
    const { login, password, captchaToken } = req.body || {};
    if (!login || !password) {
      return res.status(400).json({ error: 'Укажите логин и пароль' });
    }
    if (await isBlacklisted(clientIp, login)) {
      await writeAuthLog(clientIp, maskLogin(login), false);
      return res.status(403).json({ error: 'Доступ запрещён' });
    }
    let turnstileSecret = null;
    try {
      const captchaRows = await query('SELECT secret_key FROM security_captcha WHERE id = 1');
      const fromDb = captchaRows[0] && captchaRows[0].secret_key && String(captchaRows[0].secret_key).trim();
      turnstileSecret = fromDb || process.env.TURNSTILE_SECRET_KEY || null;
    } catch (e) {
      turnstileSecret = process.env.TURNSTILE_SECRET_KEY || null;
    }
    if (turnstileSecret) {
      if (!captchaToken || !String(captchaToken).trim()) {
        return res.status(400).json({ error: 'Пройдите проверку (капча)' });
      }
      try {
        const verifyRes = await axios.post(
          'https://challenges.cloudflare.com/turnstile/v0/siteverify',
          new URLSearchParams({
            secret: turnstileSecret,
            response: String(captchaToken).trim(),
          }),
          { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 10000 }
        );
        if (!verifyRes.data || !verifyRes.data.success) {
          return res.status(400).json({ error: 'Капча не пройдена. Попробуйте ещё раз.' });
        }
      } catch (verifyErr) {
        console.error('Turnstile verify error:', verifyErr.message);
        return res.status(400).json({ error: 'Ошибка проверки капчи' });
      }
    }
    const rows = await query(
      `SELECT pu.id, pu.employee_id AS "employeeId", pu.login, pu.email, pu.role, pu.password_hash AS "passwordHash", pu.is_active AS "isActive",
               e.name AS "employeeName", e.position AS "employeePosition", e.photo_url AS "photoUrl"
       FROM portal_users pu
       JOIN employees e ON e.id = pu.employee_id
       WHERE pu.login = $1`,
      [String(login).trim()]
    );
    if (rows.length === 0) {
      if (rateLimitIp) recordLoginFailure(rateLimitIp);
      await writeAuthLog(clientIp, maskLogin(login), false);
      return res.status(401).json({ error: 'Неверный логин или пароль' });
    }
    const row = rows[0];
    if (row.isActive === false) {
      if (rateLimitIp) recordLoginFailure(rateLimitIp);
      await writeAuthLog(clientIp, maskLogin(login), false);
      return res.status(401).json({ error: 'Учётная запись отключена' });
    }
    if (!row.passwordHash) {
      if (rateLimitIp) recordLoginFailure(rateLimitIp);
      await writeAuthLog(clientIp, maskLogin(login), false);
      return res.status(401).json({ error: 'Пароль не задан. Обратитесь к администратору.' });
    }
    const match = await bcrypt.compare(String(password), row.passwordHash);
    if (!match) {
      if (rateLimitIp) recordLoginFailure(rateLimitIp);
      await writeAuthLog(clientIp, maskLogin(login), false);
      return res.status(401).json({ error: 'Неверный логин или пароль' });
    }
    if (rateLimitIp) clearLoginAttempts(rateLimitIp);
    await writeAuthLog(clientIp, maskLogin(login), true);
    const payload = { userId: row.id, employeeId: row.employeeId, role: row.role, login: row.login };
    const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
    res.json({
      token,
      user: {
        id: row.employeeId,
        userId: row.id,
        name: row.employeeName,
        role: row.role,
        avatar: row.photoUrl ? `/uploads/photos/${path.basename(row.photoUrl)}` : null
      }
    });
  } catch (err) {
    if (rateLimitIp) recordLoginFailure(rateLimitIp);
    const login = (req.body && req.body.login) || '';
    await writeAuthLog(clientIp, maskLogin(login), false);
    console.error('Error in auth/login:', err.message || err);
    res.status(500).json({ error: 'Ошибка входа' });
  }
});

// GET /api/auth/captcha-site-key — публичный ключ капчи для формы входа (без авторизации)
app.get(`${API_PREFIX}/auth/captcha-site-key`, async (req, res) => {
  try {
    const rows = await query('SELECT site_key FROM security_captcha WHERE id = 1');
    const siteKey = (rows[0] && rows[0].site_key && String(rows[0].site_key).trim()) || process.env.VITE_TURNSTILE_SITE_KEY || null;
    res.json({ siteKey });
  } catch (err) {
    res.json({ siteKey: process.env.VITE_TURNSTILE_SITE_KEY || null });
  }
});

// POST /api/auth/guest-token — гостевой токен без учётки (для просмотра публичных отчётов)
const GUEST_TOKEN_EXPIRES_IN = process.env.JWT_GUEST_EXPIRES_IN || '24h';
app.post(`${API_PREFIX}/auth/guest-token`, async (req, res) => {
  try {
    const token = jwt.sign(
      { role: 'GUEST' },
      JWT_SECRET,
      { expiresIn: GUEST_TOKEN_EXPIRES_IN }
    );
    res.json({ token });
  } catch (err) {
    console.error('Error issuing guest token:', err);
    res.status(500).json({ error: 'Ошибка выдачи гостевого токена' });
  }
});

// Все остальные /api/* требуют авторизации
app.use(API_PREFIX, authMiddleware);

// Роутеры по доменам (разбиение монолита server.js)
const createBuildingsRouter = require('./routes/buildings');
app.use(API_PREFIX, createBuildingsRouter({ query }));

// allowedSections из детальных прав (SECTION_IDS из constants/roleAccess.js)
function allowedSectionsFromPermissions(permissions) {
  if (!permissions || !Array.isArray(permissions) || permissions.length === 0) return null;
  if (permissions.includes('all')) return ['dashboard', 'objects', 'requests', 'pr', 'finance', 'legal', 'development', 'hr', 'office', 'admin'];
  const set = new Set();
  for (const p of permissions) {
    if (SECTION_IDS.includes(p)) set.add(p);
    else if (typeof p === 'string' && p.includes('_')) {
      const section = p.split('_')[0];
      if (SECTION_IDS.includes(section)) set.add(section);
    }
  }
  return Array.from(set);
}

// Проверка доступа к разделу: загружаем allowedSections по userId и возвращаем 403, если раздела нет
function requireSection(section) {
  return async (req, res, next) => {
    if (!req.user || !req.user.userId) {
      return res.status(401).json({ error: 'Требуется авторизация' });
    }
    try {
      const rows = await query(
        'SELECT permissions, role FROM portal_users WHERE id = $1',
        [req.user.userId]
      );
      if (rows.length === 0) {
        return res.status(403).json({ error: 'Доступ к разделу запрещён' });
      }
      const row = rows[0];
      const hasCustom = row.permissions && Array.isArray(row.permissions) && row.permissions.length > 0;
      let allowedSections;
      if (hasCustom) {
        allowedSections = allowedSectionsFromPermissions(row.permissions);
      } else {
        const allowed = ROLE_ACCESS[row.role];
        allowedSections = allowed && allowed.includes('all') ? SECTION_IDS : (allowed || []);
      }
      if (!allowedSections || !allowedSections.includes(section)) {
        return res.status(403).json({ error: 'Доступ к разделу запрещён' });
      }
      next();
    } catch (err) {
      console.error('requireSection error:', err);
      res.status(500).json({ error: 'Ошибка проверки доступа' });
    }
  };
}

app.use(API_PREFIX + '/finance', requireSection('finance'));
app.use(API_PREFIX + '/office', requireSection('office'));
app.use(API_PREFIX + '/pr', requireSection('pr'));

// GET /api/auth/me
app.get(`${API_PREFIX}/auth/me`, async (req, res) => {
  try {
    const { userId, employeeId, role } = req.user;
    const rows = await query(
      `SELECT pu.id, pu.employee_id AS "employeeId", pu.login, pu.email, pu.role, pu.permissions, pu.scope,
               pu.phone AS "puPhone", pu.given_name AS "givenName", pu.family_name AS "familyName",
               pu.birth_date AS "puBirthDate", pu.language, pu.theme, pu.notification_email AS "notificationEmail",
               pu.notification_push AS "notificationPush", pu.email_verified AS "emailVerified",
               pu.last_login AS "lastLogin", pu.created_at AS "createdAt", pu.updated_at AS "updatedAt",
               e.name AS "employeeName", e.position AS "employeePosition", e.photo_url AS "photoUrl",
               e.phone AS "employeePhone", e.birth_date AS "employeeBirthDate",
               e.assigned_district_id AS "assignedDistrictId"
       FROM portal_users pu
       JOIN employees e ON e.id = pu.employee_id
       WHERE pu.id = $1`,
      [userId]
    );
    if (rows.length === 0) {
      return res.status(401).json({ error: 'Пользователь не найден' });
    }
    const row = rows[0];
    let assignedDistrictIds = [];
    try {
      const edRows = await query('SELECT district_id AS "districtId" FROM employee_districts WHERE employee_id = $1 ORDER BY district_id', [row.employeeId]);
      assignedDistrictIds = edRows.map((r) => r.districtId);
    } catch (_) {}
    if (assignedDistrictIds.length === 0 && row.assignedDistrictId) {
      assignedDistrictIds = [row.assignedDistrictId];
    }
    const hasCustomPermissions = row.permissions && Array.isArray(row.permissions) && row.permissions.length > 0;
    let allowedSections;
    if (hasCustomPermissions) {
      allowedSections = allowedSectionsFromPermissions(row.permissions);
    } else {
      const allowed = ROLE_ACCESS[row.role];
      allowedSections = allowed && allowed.includes('all') ? SECTION_IDS : (allowed || []);
    }
    const scope = (row.scope === 'own_district') ? 'own_district' : 'all';
    const avatarUrl = row.photoUrl ? `/uploads/photos/${path.basename(row.photoUrl)}` : null;
    // #region agent log
    try {
      const debugLogPath = path.join(__dirname, '..', '.cursor', 'debug.log');
      fs.appendFileSync(debugLogPath, JSON.stringify({ location: 'server.js:GET/auth/me', message: 'user data', data: { userId: row.id, role: row.role, scope: row.scope, assignedDistrictId: row.assignedDistrictId }, timestamp: Date.now(), sessionId: 'debug-session', hypothesisId: 'F' }) + '\n');
    } catch (_) {}
    // #endregion
    res.json({
      id: row.employeeId,
      userId: row.id,
      name: row.employeeName,
      givenName: row.givenName || null,
      familyName: row.familyName || null,
      email: row.email || '',
      emailVerified: !!row.emailVerified,
      phone: row.puPhone || row.employeePhone || null,
      role: row.role,
      avatar: avatarUrl,
      birthDate: row.puBirthDate ? String(row.puBirthDate).slice(0, 10) : (row.employeeBirthDate ? String(row.employeeBirthDate).slice(0, 10) : null),
      position: row.employeePosition || null,
      assignedDistrictIds: assignedDistrictIds.length ? assignedDistrictIds : null,
      assignedDistrictId: assignedDistrictIds[0] || row.assignedDistrictId || null,
      language: row.language || 'ru',
      theme: row.theme || 'light',
      notificationEmail: row.notificationEmail !== false,
      notificationPush: row.notificationPush !== false,
      lastLogin: row.lastLogin || null,
      createdAt: row.createdAt || null,
      updatedAt: row.updatedAt || null,
      allowedSections,
      permissions: hasCustomPermissions ? row.permissions : null,
      scope,
    });
  } catch (err) {
    console.error('Error in auth/me:', err);
    res.status(500).json({ error: 'Ошибка получения профиля' });
  }
});

// ----- Notifications (колокольчик): только свои уведомления -----
// GET /api/notifications — список для текущего пользователя (непрочитанные сначала, пагинация)
app.get(`${API_PREFIX}/notifications`, async (req, res) => {
  try {
    const userId = req.user.userId;
    const limit = Math.min(parseInt(req.query.limit, 10) || 50, 100);
    const offset = parseInt(req.query.offset, 10) || 0;
    const typeFilter = req.query.type ? String(req.query.type).trim() : null;
    const params = [userId];
    if (typeFilter) params.push(typeFilter);
    params.push(limit, offset);
    const whereClause = typeFilter ? ' AND type = $2' : '';
    const limitPlace = typeFilter ? 3 : 2;
    const offsetPlace = typeFilter ? 4 : 3;
    const rows = await pool.query(
      `SELECT id, type, title, body, entity_type AS "entityType", entity_id AS "entityId", payload, read_at AS "readAt", created_at AS "createdAt"
       FROM notifications
       WHERE user_id = $1${whereClause}
       ORDER BY read_at ASC NULLS FIRST, created_at DESC
       LIMIT $${limitPlace} OFFSET $${offsetPlace}`,
      params
    );
    const unreadCount = await pool.query(
      'SELECT COUNT(*)::int AS c FROM notifications WHERE user_id = $1 AND read_at IS NULL',
      [userId]
    );
    res.setHeader('X-Unread-Count', String(unreadCount.rows[0].c));
    res.json(rows.rows);
  } catch (err) {
    console.error('Error GET /notifications:', err);
    res.status(500).json({ error: 'Ошибка загрузки уведомлений' });
  }
});

// GET /api/notifications/unread-count — только счётчик непрочитанных
app.get(`${API_PREFIX}/notifications/unread-count`, async (req, res) => {
  try {
    const userId = req.user.userId;
    const r = await pool.query(
      'SELECT COUNT(*)::int AS c FROM notifications WHERE user_id = $1 AND read_at IS NULL',
      [userId]
    );
    res.json({ count: r.rows[0].c });
  } catch (err) {
    console.error('Error GET /notifications/unread-count:', err);
    res.status(500).json({ count: 0 });
  }
});

// PATCH /api/notifications/:id — пометить прочитанным (только своё)
app.patch(`${API_PREFIX}/notifications/:id`, async (req, res) => {
  try {
    const userId = req.user.userId;
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const r = await pool.query(
      `UPDATE notifications SET read_at = NOW() WHERE id = $1 AND user_id = $2 RETURNING id`,
      [id, userId]
    );
    if (r.rows.length === 0) return res.status(404).json({ error: 'Уведомление не найдено' });
    res.json({ id: r.rows[0].id, readAt: new Date().toISOString() });
  } catch (err) {
    console.error('Error PATCH /notifications/:id:', err);
    res.status(500).json({ error: 'Ошибка обновления уведомления' });
  }
});

// PATCH /api/notifications/read-all — пометить все прочитанными для текущего пользователя
app.patch(`${API_PREFIX}/notifications/read-all`, async (req, res) => {
  try {
    const userId = req.user.userId;
    await pool.query(
      'UPDATE notifications SET read_at = NOW() WHERE user_id = $1 AND read_at IS NULL',
      [userId]
    );
    res.json({ success: true });
  } catch (err) {
    console.error('Error PATCH /notifications/read-all:', err);
    res.status(500).json({ error: 'Ошибка обновления уведомлений' });
  }
});

// PUT /api/auth/profile — обновление профиля текущего пользователя
app.put(`${API_PREFIX}/auth/profile`, async (req, res) => {
  try {
    const { userId, employeeId } = req.user;
    const body = req.body || {};
    const updates = [];
    const values = [];
    let idx = 1;

    // Поля в employees: name, phone, birth_date
    if (body.name !== undefined) {
      updates.push(`name = $${idx++}`);
      values.push(String(body.name).trim());
    }
    if (body.phone !== undefined) {
      updates.push(`phone = $${idx++}`);
      values.push(String(body.phone).trim() || null);
    }
    if (body.birthDate !== undefined) {
      updates.push(`birth_date = $${idx++}`);
      values.push(body.birthDate ? String(body.birthDate).trim() : null);
    }

    if (updates.length > 0) {
      values.push(employeeId);
      await query(
        `UPDATE employees SET ${updates.join(', ')}, updated_at = NOW() WHERE id = $${idx}`,
        values
      );
    }

    // Поля в portal_users: email, given_name, family_name, language, theme, notification_email, notification_push
    const puUpdates = [];
    const puValues = [];
    let puIdx = 1;
    if (body.email !== undefined) {
      const email = String(body.email).trim() || null;
      if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
        return res.status(400).json({ error: 'Некорректный формат email' });
      }
      puUpdates.push(`email = $${puIdx++}`);
      puValues.push(email);
    }
    if (body.givenName !== undefined) {
      puUpdates.push(`given_name = $${puIdx++}`);
      puValues.push(body.givenName ? String(body.givenName).trim() : null);
    }
    if (body.familyName !== undefined) {
      puUpdates.push(`family_name = $${puIdx++}`);
      puValues.push(body.familyName ? String(body.familyName).trim() : null);
    }
    if (body.language !== undefined) {
      puUpdates.push(`language = $${puIdx++}`);
      puValues.push(String(body.language).slice(0, 10) || 'ru');
    }
    if (body.theme !== undefined) {
      puUpdates.push(`theme = $${puIdx++}`);
      puValues.push(['light', 'dark', 'system'].includes(body.theme) ? body.theme : 'light');
    }
    if (body.notificationEmail !== undefined) {
      puUpdates.push(`notification_email = $${puIdx++}`);
      puValues.push(!!body.notificationEmail);
    }
    if (body.notificationPush !== undefined) {
      puUpdates.push(`notification_push = $${puIdx++}`);
      puValues.push(!!body.notificationPush);
    }

    if (puUpdates.length > 0) {
      puValues.push(userId);
      await query(
        `UPDATE portal_users SET ${puUpdates.join(', ')}, updated_at = NOW() WHERE id = $${puIdx}`,
        puValues
      );
    }

    // Возвращаем обновлённый профиль через auth/me логику
    const rows = await query(
      `SELECT pu.id, pu.employee_id AS "employeeId", pu.login, pu.email, pu.role, pu.permissions, pu.scope,
               pu.phone AS "puPhone", pu.given_name AS "givenName", pu.family_name AS "familyName",
               pu.birth_date AS "puBirthDate", pu.language, pu.theme, pu.notification_email AS "notificationEmail",
               pu.notification_push AS "notificationPush", pu.email_verified AS "emailVerified",
               pu.last_login AS "lastLogin", pu.created_at AS "createdAt", pu.updated_at AS "updatedAt",
               e.name AS "employeeName", e.position AS "employeePosition", e.photo_url AS "photoUrl",
               e.phone AS "employeePhone", e.birth_date AS "employeeBirthDate",
               e.assigned_district_id AS "assignedDistrictId"
       FROM portal_users pu
       JOIN employees e ON e.id = pu.employee_id
       WHERE pu.id = $1`,
      [userId]
    );
    if (rows.length === 0) {
      return res.status(401).json({ error: 'Пользователь не найден' });
    }
    const row = rows[0];
    const hasCustomPermissions = row.permissions && Array.isArray(row.permissions) && row.permissions.length > 0;
    let allowedSections;
    if (hasCustomPermissions) {
      allowedSections = allowedSectionsFromPermissions(row.permissions);
    } else {
      const allowed = ROLE_ACCESS[row.role];
      allowedSections = allowed && allowed.includes('all') ? SECTION_IDS : (allowed || []);
    }
    const scope = (row.scope === 'own_district') ? 'own_district' : 'all';
    const avatarUrl = row.photoUrl ? `/uploads/photos/${path.basename(row.photoUrl)}` : null;
    res.json({
      id: row.employeeId,
      userId: row.id,
      name: row.employeeName,
      givenName: row.givenName || null,
      familyName: row.familyName || null,
      email: row.email || '',
      emailVerified: !!row.emailVerified,
      phone: row.puPhone || row.employeePhone || null,
      role: row.role,
      avatar: avatarUrl,
      birthDate: row.puBirthDate ? String(row.puBirthDate).slice(0, 10) : (row.employeeBirthDate ? String(row.employeeBirthDate).slice(0, 10) : null),
      position: row.employeePosition || null,
      assignedDistrictId: row.assignedDistrictId || null,
      language: row.language || 'ru',
      theme: row.theme || 'light',
      notificationEmail: row.notificationEmail !== false,
      notificationPush: row.notificationPush !== false,
      lastLogin: row.lastLogin || null,
      createdAt: row.createdAt || null,
      updatedAt: row.updatedAt || null,
      allowedSections,
      permissions: hasCustomPermissions ? row.permissions : null,
      scope,
    });
  } catch (err) {
    console.error('Error in auth/profile:', err);
    res.status(500).json({ error: 'Ошибка обновления профиля' });
  }
});

// POST /api/auth/profile/photo — загрузка фото профиля текущего пользователя
app.post(`${API_PREFIX}/auth/profile/photo`, uploadPhoto.single('photo'), async (req, res) => {
  try {
    const { employeeId } = req.user;
    if (!req.file) {
      return res.status(400).json({ error: 'Файл не загружен' });
    }
    const existing = await query('SELECT id, photo_url FROM employees WHERE id = $1', [employeeId]);
    if (existing.length === 0) {
      fs.unlink(req.file.path, (err) => { if (err) console.error('Ошибка удаления файла:', err); });
      return res.status(404).json({ error: 'Сотрудник не найден' });
    }
    const oldPhotoUrl = existing[0].photo_url;
    if (oldPhotoUrl) {
      const oldPath = path.join(__dirname, oldPhotoUrl.replace(/^\//, '').replace('uploads/', 'uploads/'));
      if (fs.existsSync(oldPath)) {
        fs.unlink(oldPath, (err) => { if (err) console.error('Ошибка удаления старого фото:', err); });
      }
    }
    const photoUrl = `/uploads/photos/${req.file.filename}`;
    await query('UPDATE employees SET photo_url = $1, updated_at = NOW() WHERE id = $2', [photoUrl, employeeId]);
    res.json({ success: true, photoUrl });
  } catch (err) {
    console.error('Error uploading profile photo:', err);
    res.status(500).json({ error: 'Ошибка загрузки фото' });
  }
});

// DELETE /api/auth/profile/photo — удаление фото профиля
app.delete(`${API_PREFIX}/auth/profile/photo`, async (req, res) => {
  try {
    const { employeeId } = req.user;
    const employee = await query('SELECT photo_url FROM employees WHERE id = $1', [employeeId]);
    if (employee.length === 0) {
      return res.status(404).json({ error: 'Сотрудник не найден' });
    }
    if (employee[0].photo_url) {
      const filePath = path.join(__dirname, employee[0].photo_url.replace(/^\//, '').replace(/^uploads\//, 'uploads/'));
      if (fs.existsSync(filePath)) {
        fs.unlinkSync(filePath);
      }
    }
    await query('UPDATE employees SET photo_url = NULL, updated_at = NOW() WHERE id = $1', [employeeId]);
    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting profile photo:', err);
    res.status(500).json({ error: 'Ошибка удаления фото' });
  }
});

// PUT /api/auth/change-password — смена пароля текущего пользователя
app.put(`${API_PREFIX}/auth/change-password`, async (req, res) => {
  try {
    const { userId } = req.user;
    const { oldPassword, newPassword } = req.body || {};
    if (!oldPassword || !newPassword) {
      return res.status(400).json({ error: 'Укажите старый и новый пароль' });
    }
    if (String(newPassword).length < 8) {
      return res.status(400).json({ error: 'Новый пароль должен содержать не менее 8 символов' });
    }
    if (!/^(?=.*[a-zA-Z])(?=.*\d)/.test(String(newPassword))) {
      return res.status(400).json({ error: 'Пароль должен содержать буквы и цифры' });
    }
    const rows = await query('SELECT password_hash AS "passwordHash" FROM portal_users WHERE id = $1', [userId]);
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Пользователь не найден' });
    }
    const match = await bcrypt.compare(String(oldPassword), rows[0].passwordHash || '');
    if (!match) {
      return res.status(401).json({ error: 'Неверный текущий пароль' });
    }
    const passwordHash = await bcrypt.hash(String(newPassword), 10);
    await query(
      'UPDATE portal_users SET password_hash = $1, password_changed_at = NOW(), updated_at = NOW() WHERE id = $2',
      [passwordHash, userId]
    );
    res.json({ success: true, message: 'Пароль успешно изменён' });
  } catch (err) {
    console.error('Error changing password:', err);
    res.status(500).json({ error: 'Ошибка смены пароля' });
  }
});

// GET /api/auth/preferences — настройки текущего пользователя
app.get(`${API_PREFIX}/auth/preferences`, async (req, res) => {
  try {
    const { userId } = req.user;
    const rows = await query(
      `SELECT language, theme, notification_email AS "notificationEmail", notification_push AS "notificationPush"
       FROM portal_users WHERE id = $1`,
      [userId]
    );
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Пользователь не найден' });
    }
    const r = rows[0];
    res.json({
      language: r.language || 'ru',
      theme: r.theme || 'light',
      notificationEmail: r.notificationEmail !== false,
      notificationPush: r.notificationPush !== false
    });
  } catch (err) {
    console.error('Error fetching preferences:', err);
    res.status(500).json({ error: 'Ошибка получения настроек' });
  }
});

// PUT /api/auth/preferences — обновление настроек
app.put(`${API_PREFIX}/auth/preferences`, async (req, res) => {
  try {
    const { userId } = req.user;
    const body = req.body || {};
    const updates = [];
    const values = [];
    let idx = 1;
    if (body.language !== undefined) {
      updates.push(`language = $${idx++}`);
      values.push(String(body.language).slice(0, 10) || 'ru');
    }
    if (body.theme !== undefined) {
      updates.push(`theme = $${idx++}`);
      values.push(['light', 'dark', 'system'].includes(body.theme) ? body.theme : 'light');
    }
    if (body.notificationEmail !== undefined) {
      updates.push(`notification_email = $${idx++}`);
      values.push(!!body.notificationEmail);
    }
    if (body.notificationPush !== undefined) {
      updates.push(`notification_push = $${idx++}`);
      values.push(!!body.notificationPush);
    }
    if (updates.length === 0) {
      return res.status(400).json({ error: 'Нет данных для обновления' });
    }
    values.push(userId);
    await query(`UPDATE portal_users SET ${updates.join(', ')}, updated_at = NOW() WHERE id = $${idx}`, values);
    const rows = await query(
      `SELECT language, theme, notification_email AS "notificationEmail", notification_push AS "notificationPush"
       FROM portal_users WHERE id = $1`,
      [userId]
    );
    res.json({
      language: rows[0]?.language || 'ru',
      theme: rows[0]?.theme || 'light',
      notificationEmail: rows[0]?.notificationEmail !== false,
      notificationPush: rows[0]?.notificationPush !== false
    });
  } catch (err) {
    console.error('Error updating preferences:', err);
    res.status(500).json({ error: 'Ошибка обновления настроек' });
  }
});

// ========= ИИ-ЧАТ =========
// GET /api/ai/status — включён ли ИИ (для отображения кнопки в интерфейсе)
app.get(`${API_PREFIX}/ai/status`, async (req, res) => {
  try {
    const rows = await query(
      'SELECT enabled, config FROM integration_settings WHERE key = $1',
      ['ai_chat']
    );
    if (rows.length === 0) {
      return res.json({ enabled: false });
    }
    const r = rows[0];
    const hasUrl = r.config && r.config.url && String(r.config.url).trim();
    res.json({ enabled: r.enabled !== false && !!hasUrl });
  } catch (err) {
    res.json({ enabled: false });
  }
});

// GET /api/ai/conversations — список диалогов текущего пользователя
app.get(`${API_PREFIX}/ai/conversations`, async (req, res) => {
  try {
    const userId = req.user.userId;
    const rows = await query(
      `SELECT id, title, created_at AS "createdAt"
       FROM ai_conversations
       WHERE user_id = $1
       ORDER BY created_at DESC`,
      [userId]
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching AI conversations:', err);
    res.status(500).json({ error: 'Ошибка загрузки диалогов' });
  }
});

// POST /api/ai/conversations — создать диалог
app.post(`${API_PREFIX}/ai/conversations`, async (req, res) => {
  try {
    const userId = req.user.userId;
    const { title } = req.body || {};
    const result = await query(
      `INSERT INTO ai_conversations (user_id, title) VALUES ($1, $2)
       RETURNING id, title, created_at AS "createdAt"`,
      [userId, title || null]
    );
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating AI conversation:', err);
    res.status(500).json({ error: 'Ошибка создания диалога' });
  }
});

// GET /api/ai/conversations/:id/messages — сообщения диалога (только свой)
app.get(`${API_PREFIX}/ai/conversations/:id/messages`, async (req, res) => {
  try {
    const userId = req.user.userId;
    const conversationId = parseInt(req.params.id, 10);
    if (isNaN(conversationId)) {
      return res.status(400).json({ error: 'Некорректный id диалога' });
    }
    const conv = await query(
      'SELECT id FROM ai_conversations WHERE id = $1 AND user_id = $2',
      [conversationId, userId]
    );
    if (conv.length === 0) {
      return res.status(404).json({ error: 'Диалог не найден' });
    }
    const rows = await query(
      `SELECT id, role, content, tool_calls_json AS "toolCallsJson", created_at AS "createdAt"
       FROM ai_messages
       WHERE conversation_id = $1
       ORDER BY created_at ASC`,
      [conversationId]
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching AI messages:', err);
    res.status(500).json({ error: 'Ошибка загрузки сообщений' });
  }
});

// POST /api/ai/chat — отправить сообщение, получить ответ от ИИ (с опциональными tool_calls)
app.post(`${API_PREFIX}/ai/chat`, async (req, res) => {
  try {
    const userId = req.user.userId;
    const { conversationId: rawConvId, message } = req.body || {};
    if (!message || typeof message !== 'string' || !message.trim()) {
      return res.status(400).json({ error: 'Укажите текст сообщения' });
    }
    let conversationId = rawConvId != null ? parseInt(rawConvId, 10) : null;
    if (conversationId != null && isNaN(conversationId)) {
      return res.status(400).json({ error: 'Некорректный id диалога' });
    }

    if (conversationId == null) {
      const created = await query(
        'INSERT INTO ai_conversations (user_id) VALUES ($1) RETURNING id',
        [userId]
      );
      conversationId = created[0].id;
    } else {
      const own = await query(
        'SELECT id FROM ai_conversations WHERE id = $1 AND user_id = $2',
        [conversationId, userId]
      );
      if (own.length === 0) {
        return res.status(404).json({ error: 'Диалог не найден' });
      }
    }

    await query(
      `INSERT INTO ai_messages (conversation_id, role, content) VALUES ($1, 'user', $2)`,
      [conversationId, message.trim()]
    );

    let aiChatUrl = process.env.AI_CHAT_URL || 'https://ai.iieasy.ru/v1/chat/completions';
    let aiApiKey = process.env.AI_API_KEY || '';
    try {
      const aiRows = await query(
        'SELECT enabled, config FROM integration_settings WHERE key = $1',
        ['ai_chat']
      );
      if (aiRows.length) {
        const row = aiRows[0];
        if (row.enabled === false) {
          aiChatUrl = '';
        } else if (row.config && row.config.url && String(row.config.url).trim()) {
          aiChatUrl = String(row.config.url).trim();
          aiApiKey = row.config.apiKey != null ? String(row.config.apiKey) : '';
        }
      }
    } catch (e) {
      // keep env defaults
    }
    const baseUrl = process.env.BACKEND_BASE_URL || `http://localhost:${PORT}`;
    const getTokenForUser = (u) => jwt.sign(
      { userId: u.userId, employeeId: u.employeeId, role: u.role },
      JWT_SECRET,
      { expiresIn: '5m' }
    );
    let userContext = { userName: 'Пользователь', role: '', allowedSections: [] };
    try {
      const meRows = await query(
        `SELECT e.name, pu.role, pu.permissions
         FROM portal_users pu
         JOIN employees e ON e.id = pu.employee_id
         WHERE pu.id = $1`,
        [userId]
      );
      if (meRows.length) {
        const r = meRows[0];
        let allowed = r.permissions && Array.isArray(r.permissions) && r.permissions.length
          ? (r.permissions.includes('all') ? ['dashboard', 'objects', 'requests', 'pr', 'finance', 'legal', 'development', 'hr', 'office', 'admin'] : r.permissions)
          : (ROLE_ACCESS[r.role] || []);
        if (allowed && allowed.includes('all')) {
          allowed = ['dashboard', 'objects', 'requests', 'pr', 'finance', 'legal', 'development', 'hr', 'office', 'admin'];
        }
        userContext = { userName: r.name || 'Пользователь', role: r.role || '', allowedSections: allowed || [] };
      }
    } catch (e) {
      // keep defaults
    }

    let assistantContent;
    let toolResults = [];
    const aiEnabled = !!aiChatUrl;
    if (aiEnabled) {
      try {
        const result = await aiChatService.getAIResponse({
          query,
          conversationId,
          newUserMessage: message.trim(),
          user: req.user,
          userContext,
          runToolContext: { baseUrl, apiPrefix: API_PREFIX, getTokenForUser },
          aiChatUrl,
          aiApiKey
        });
        assistantContent = result.assistantMessage || 'Нет ответа.';
        toolResults = result.toolResults || [];
      } catch (aiErr) {
        console.error('AI chat error:', aiErr.message || aiErr);
        assistantContent = 'ИИ временно недоступен. Попробуйте позже. Ваше сообщение сохранено.';
      }
    } else {
      assistantContent = 'ИИ выключен или не настроен. Включите и укажите адрес в Панели управления → ИИ. Ваше сообщение сохранено.';
    }

    await query(
      `INSERT INTO ai_messages (conversation_id, role, content) VALUES ($1, 'assistant', $2)`,
      [conversationId, assistantContent]
    );

    res.json({
      conversationId,
      assistantMessage: assistantContent,
      toolResults
    });
  } catch (err) {
    console.error('Error in AI chat:', err);
    res.status(500).json({ error: 'Ошибка отправки сообщения' });
  }
});

// ----------- API МАРШРУТЫ -----------

// Кэш GET /api/districts (30 сек), инвалидация при изменении участков
const DISTRICTS_CACHE_TTL_MS = 30 * 1000;
let districtsCache = { data: null, expiresAt: 0 };
function invalidateDistrictsCache() {
  districtsCache = { data: null, expiresAt: 0 };
}

// GET /api/districts -> список участков
app.get(`${API_PREFIX}/districts`, async (req, res) => {
  try {
    const now = Date.now();
    if (districtsCache.data !== null && now < districtsCache.expiresAt) {
      return res.json(districtsCache.data);
    }
    const rows = await query(
      `SELECT 
        id, 
        name, 
        manager_name AS "managerName",
        COALESCE(inventory, '[]'::jsonb) AS inventory
       FROM districts 
       ORDER BY id`
    );
    // #region agent log
    try {
      const debugLogPath = path.join(__dirname, '..', '.cursor', 'debug.log');
      fs.appendFileSync(debugLogPath, JSON.stringify({ location: 'server.js:GET/districts', message: 'districts list', data: { count: rows.length, districtIds: rows.map(r => r.id) }, timestamp: Date.now(), sessionId: 'debug-session', hypothesisId: 'G' }) + '\n');
    } catch (_) {}
    // #endregion
    districtsCache = { data: rows, expiresAt: now + DISTRICTS_CACHE_TTL_MS };
    res.json(rows);
  } catch (err) {
    console.error('Error fetching districts:', err);
    res.status(500).json({ error: 'Failed to fetch districts' });
  }
});

// Значение по умолчанию для «начальник участка не назначен»
const DISTRICT_MANAGER_UNASSIGNED = 'Не назначен';

// POST /api/districts -> создание нового участка (managerName может быть пустым — тогда «Не назначен»)
app.post(`${API_PREFIX}/districts`, async (req, res) => {
  const { name, managerName, inventory } = req.body || {};

  if (!name || typeof name !== 'string' || !name.trim()) {
    return res.status(400).json({ error: 'name обязателен' });
  }

  const id = `d-${Date.now()}`;
  const manager = (managerName != null && String(managerName).trim()) ? String(managerName).trim() : DISTRICT_MANAGER_UNASSIGNED;

  try {
    await query(
      'INSERT INTO districts (id, name, manager_name, inventory) VALUES ($1, $2, $3, $4)',
      [id, name.trim(), manager, JSON.stringify(inventory || [])]
    );
    invalidateDistrictsCache();
    res.status(201).json({ id, name: name.trim(), managerName: manager, inventory: inventory || [] });
  } catch (err) {
    console.error('Error creating district:', err);
    res.status(500).json({ error: 'Failed to create district' });
  }
});

// PUT /api/districts/:id -> обновление участка (включая inventory)
app.put(`${API_PREFIX}/districts/:id`, async (req, res) => {
  const { id } = req.params;
  const { name, managerName, inventory } = req.body || {};

  try {
    const updates = [];
    const values = [];
    let paramIndex = 1;

    if (name !== undefined) {
      updates.push(`name = $${paramIndex++}`);
      values.push(name);
    }
    if (managerName !== undefined) {
      updates.push(`manager_name = $${paramIndex++}`);
      values.push(managerName);
    }
    if (inventory !== undefined) {
      updates.push(`inventory = $${paramIndex++}`);
      values.push(JSON.stringify(inventory));
    }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    values.push(id);
    const queryText = `UPDATE districts SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING id, name, manager_name AS "managerName", COALESCE(inventory, '[]'::jsonb) AS inventory`;
    
    const result = await query(queryText, values);
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Участок не найден' });
    }
    invalidateDistrictsCache();
    res.json(result[0]);
  } catch (err) {
    console.error('Error updating district:', err);
    res.status(500).json({ error: 'Failed to update district' });
  }
});

// DELETE /api/districts/:id -> удаление участка
app.delete(`${API_PREFIX}/districts/:id`, async (req, res) => {
  const { id } = req.params;

  try {
    // Проверяем, есть ли дома, привязанные к этому участку
    const buildingsResult = await query(
      `SELECT COUNT(*) as count FROM buildings WHERE data->>'districtId' = $1`,
      [id]
    );

    const buildingCount = parseInt(buildingsResult[0].count);
    if (buildingCount > 0) {
      return res.status(400).json({ 
        error: `Невозможно удалить участок: к нему привязано ${buildingCount} домов. Сначала переместите дома в другой участок.` 
      });
    }

    // Удаляем участок
    const deleteResult = await query(
      'DELETE FROM districts WHERE id = $1 RETURNING id',
      [id]
    );

    if (deleteResult.length === 0) {
      return res.status(404).json({ error: 'Участок не найден' });
    }
    invalidateDistrictsCache();
    res.json({ success: true, message: 'Участок удален' });
  } catch (err) {
    console.error('Error deleting district:', err);
    res.status(500).json({ error: 'Failed to delete district' });
  }
});

// ----------- POSITIONS (справочник должностей) -----------

// GET /api/positions -> список должностей
app.get(`${API_PREFIX}/positions`, async (req, res) => {
  try {
    const rows = await query(
      `SELECT id, name, is_managerial AS "isManagerial", sort_order AS "sortOrder", created_at AS "createdAt", updated_at AS "updatedAt"
       FROM positions ORDER BY sort_order ASC, name ASC`
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching positions:', err);
    res.status(500).json({ error: 'Failed to fetch positions' });
  }
});

// POST /api/positions -> создание должности
app.post(`${API_PREFIX}/positions`, async (req, res) => {
  const { name, isManagerial, sortOrder } = req.body || {};
  if (!name || typeof name !== 'string' || !name.trim()) {
    return res.status(400).json({ error: 'name обязателен' });
  }
  const id = `pos-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  try {
    await query(
      'INSERT INTO positions (id, name, is_managerial, sort_order) VALUES ($1, $2, $3, $4)',
      [id, name.trim(), !!isManagerial, typeof sortOrder === 'number' ? sortOrder : 0]
    );
    const [row] = await query(
      `SELECT id, name, is_managerial AS "isManagerial", sort_order AS "sortOrder" FROM positions WHERE id = $1`,
      [id]
    );
    res.status(201).json(row);
  } catch (err) {
    console.error('Error creating position:', err);
    res.status(500).json({ error: 'Failed to create position' });
  }
});

// PUT /api/positions/:id -> обновление должности
app.put(`${API_PREFIX}/positions/:id`, async (req, res) => {
  const { id } = req.params;
  const { name, isManagerial, sortOrder } = req.body || {};
  try {
    const updates = [];
    const values = [];
    let paramIndex = 1;
    if (name !== undefined) {
      updates.push(`name = $${paramIndex++}`);
      values.push(typeof name === 'string' ? name.trim() : name);
    }
    if (isManagerial !== undefined) {
      updates.push(`is_managerial = $${paramIndex++}`);
      values.push(!!isManagerial);
    }
    if (sortOrder !== undefined) {
      updates.push(`sort_order = $${paramIndex++}`);
      values.push(typeof sortOrder === 'number' ? sortOrder : 0);
    }
    if (updates.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }
    updates.push(`updated_at = NOW()`);
    values.push(id);
    const result = await query(
      `UPDATE positions SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING id, name, is_managerial AS "isManagerial", sort_order AS "sortOrder"`,
      values
    );
    if (result.length === 0) {
      return res.status(404).json({ error: 'Должность не найдена' });
    }
    res.json(result[0]);
  } catch (err) {
    console.error('Error updating position:', err);
    res.status(500).json({ error: 'Failed to update position' });
  }
});

// DELETE /api/positions/:id -> удаление должности
app.delete(`${API_PREFIX}/positions/:id`, async (req, res) => {
  const { id } = req.params;
  try {
    const result = await query('DELETE FROM positions WHERE id = $1 RETURNING id', [id]);
    if (result.length === 0) {
      return res.status(404).json({ error: 'Должность не найдена' });
    }
    res.json({ success: true, message: 'Должность удалена' });
  } catch (err) {
    console.error('Error deleting position:', err);
    res.status(500).json({ error: 'Failed to delete position' });
  }
});

// ----------- EMPLOYEES API -----------

// POST /api/employees/:id/photo -> загрузка фото сотрудника
app.post(`${API_PREFIX}/employees/:id/photo`, uploadPhoto.single('photo'), async (req, res) => {
  try {
    const { id } = req.params;
    
    if (!req.file) {
      return res.status(400).json({ error: 'Файл не загружен' });
    }

    // Проверяем существование сотрудника
    const existing = await query('SELECT id FROM employees WHERE id = $1', [id]);
    if (existing.length === 0) {
      // Удаляем загруженный файл, если сотрудник не найден
      fs.unlink(req.file.path, (err) => {
        if (err) console.error('Ошибка удаления файла:', err);
      });
      return res.status(404).json({ error: 'Сотрудник не найден' });
    }

    // Путь к файлу относительно папки uploads
    const photoUrl = `/uploads/photos/${req.file.filename}`;

    // Обновляем photo_url в таблице employees
    await query(
      'UPDATE employees SET photo_url = $1, updated_at = NOW() WHERE id = $2',
      [photoUrl, id]
    );

    res.json({ success: true, photoUrl });
  } catch (err) {
    console.error('Error uploading photo:', err);
    res.status(500).json({ error: 'Failed to upload photo' });
  }
});

// DELETE /api/employees/:id/photo -> удаление фото сотрудника
app.delete(`${API_PREFIX}/employees/:id/photo`, async (req, res) => {
  try {
    const { id } = req.params;

    // Получаем текущий photo_url
    const employee = await query('SELECT photo_url FROM employees WHERE id = $1', [id]);
    if (employee.length === 0) {
      return res.status(404).json({ error: 'Сотрудник не найден' });
    }

    // Удаляем файл, если он существует
    if (employee[0].photo_url) {
      const filePath = path.join(__dirname, employee[0].photo_url.replace('/uploads/', 'uploads/'));
      if (fs.existsSync(filePath)) {
        fs.unlink(filePath, (err) => {
          if (err) console.error('Ошибка удаления файла:', err);
        });
      }
    }

    // Обновляем photo_url в БД
    await query(
      'UPDATE employees SET photo_url = NULL, updated_at = NOW() WHERE id = $1',
      [id]
    );

    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting photo:', err);
    res.status(500).json({ error: 'Failed to delete photo' });
  }
});

// GET /api/employees/list -> лёгкий список id+name для выпадающих списков (участники совещаний и т.д.)
app.get(`${API_PREFIX}/employees/list`, async (req, res) => {
  try {
    const rows = await query(
      `SELECT id, name FROM employees WHERE status = 'active' ORDER BY name`
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching employees list:', err);
    res.status(500).json({ error: 'Failed to fetch employees list' });
  }
});

// --- Зоны ответственности (разделы/подразделы модулей → сотрудники) ---
// GET /api/responsibility -> список привязок { assignments: [ { employeeId, section, subId } ] }, опционально ?section=hr
app.get(`${API_PREFIX}/responsibility`, async (req, res) => {
  try {
    await ensureEmployeeResponsibilityTable();
    const { section } = req.query;
    let sql = `SELECT employee_id AS "employeeId", section, sub_id AS "subId" FROM employee_responsibility`;
    const params = [];
    if (section) {
      params.push(section);
      sql += ` WHERE section = $1`;
    }
    sql += ` ORDER BY section, sub_id, employee_id`;
    const rows = await query(sql, params);
    res.json({ assignments: rows });
  } catch (err) {
    // Если таблицы нет (42P01) — создаём и возвращаем пустой список, чтобы UI загрузился
    if (err.code === '42P01' || (err.message && err.message.includes('employee_responsibility'))) {
      try {
        await ensureEmployeeResponsibilityTable();
        const rows = await query(`SELECT employee_id AS "employeeId", section, sub_id AS "subId" FROM employee_responsibility ORDER BY section, sub_id, employee_id`, []);
        return res.json({ assignments: rows });
      } catch (e2) {
        console.error('Error creating responsibility table:', e2);
        return res.json({ assignments: [] });
      }
    }
    console.error('Error fetching responsibility:', err);
    res.status(500).json({ error: 'Failed to fetch responsibility' });
  }
});

// PUT /api/responsibility -> установить ответственных за зону (section + subId). body: { section, subId, employeeIds: string[] }
app.put(`${API_PREFIX}/responsibility`, async (req, res) => {
  try {
    await ensureEmployeeResponsibilityTable();
    const { section, subId, employeeIds } = req.body || {};
    if (!section || !subId || !Array.isArray(employeeIds)) {
      return res.status(400).json({ error: 'Required: section, subId, employeeIds (array)' });
    }
    await query('DELETE FROM employee_responsibility WHERE section = $1 AND sub_id = $2', [section, subId]);
    const ids = employeeIds.filter(Boolean);
    for (const employeeId of ids) {
      await query(
        `INSERT INTO employee_responsibility (employee_id, section, sub_id) VALUES ($1, $2, $3) ON CONFLICT (employee_id, section, sub_id) DO NOTHING`,
        [employeeId, section, subId]
      );
    }
    res.json({ ok: true, section, subId, count: ids.length });
  } catch (err) {
    console.error('Error updating responsibility:', err);
    res.status(500).json({ error: 'Failed to update responsibility' });
  }
});

// GET /api/employees -> список сотрудников
app.get(`${API_PREFIX}/employees`, async (req, res) => {
  try {
    const rows = await query(`
      SELECT 
        e.id,
        e.name,
        e.position,
        e.phone,
        e.status,
        e.salary,
        e.assigned_district_id AS "assignedDistrictId",
        e.manager_id AS "managerId",
        e.birth_date AS "birthDate",
        e.photo_url AS "employeePhotoUrl",
        COALESCE(e.photo_url, pu.photo_url) AS "photoUrl",
        e.registration_date AS "registrationDate",
        COALESCE(
          json_agg(
            DISTINCT jsonb_build_object(
              'messenger', ml.messenger,
              'login', ml.login
            )
          ) FILTER (WHERE ml.id IS NOT NULL),
          '[]'
        ) AS "messengerLogins"
      FROM employees e
      LEFT JOIN employee_messenger_logins ml ON e.id = ml.employee_id
      LEFT JOIN portal_users pu ON e.id = pu.employee_id
      GROUP BY e.id, pu.photo_url
      ORDER BY e.name
    `);

    // Назначения сотрудников на участки (многие ко многим)
    let districtIdsByEmployee = {};
    try {
      const edRows = await query('SELECT employee_id AS "employeeId", district_id AS "districtId" FROM employee_districts');
      for (const r of edRows) {
        if (!districtIdsByEmployee[r.employeeId]) districtIdsByEmployee[r.employeeId] = [];
        districtIdsByEmployee[r.employeeId].push(r.districtId);
      }
    } catch (edErr) {
      // Таблица employee_districts может отсутствовать до миграции
    }

    // Получаем HR данные для каждого сотрудника
    const employeesWithHrData = await Promise.all(rows.map(async (emp) => {
      // Паспортные данные
      const passportRows = await query(
        'SELECT series, number, issued_by AS "issuedBy", issued_date AS "issuedDate", registration_address AS "registrationAddress" FROM employee_passport_data WHERE employee_id = $1',
        [emp.id]
      );

      // Трудовая книжка
      const laborBookRows = await query(
        'SELECT number, series FROM employee_labor_books WHERE employee_id = $1',
        [emp.id]
      );
      const laborBookEntries = await query(
        'SELECT date, organization, position FROM labor_book_entries WHERE labor_book_id = (SELECT id FROM employee_labor_books WHERE employee_id = $1) ORDER BY date',
        [emp.id]
      );

      // Справки
      const certificatesRows = await query(
        'SELECT type, requested_date AS "requestedDate", issued_date AS "issuedDate", status FROM employee_certificates WHERE employee_id = $1',
        [emp.id]
      );

      // Прочие документы
      const otherDocsRows = await query(
        'SELECT name, type, date, file_url AS "fileUrl" FROM employee_other_documents WHERE employee_id = $1',
        [emp.id]
      );

      // Бухгалтерская информация
      const accountingRows = await query(
        'SELECT inn, snils, bank_name AS "bankName", bank_account AS "bankAccount", correspondent_account AS "correspondentAccount", bik, tax_id AS "taxId" FROM employee_accounting_data WHERE employee_id = $1',
        [emp.id]
      );

      // Договоры
      const contractsRows = await query(
        'SELECT id, contract_type AS "contractType", contract_number AS "contractNumber", start_date AS "startDate", end_date AS "endDate", probation_period_days AS "probationPeriodDays", work_schedule AS "workSchedule", work_mode AS "workMode", contract_terms AS "contractTerms" FROM employee_contracts WHERE employee_id = $1 ORDER BY start_date DESC',
        [emp.id]
      );

      // Отпуска
      const vacationsRows = await query(
        `SELECT id, employee_id AS "employeeId", start_date AS "startDate", end_date AS "endDate", days_count AS "daysCount", 
         vacation_type AS "vacationType", status, approved_by AS "approvedBy", approved_at AS "approvedAt", 
         requires_approval AS "requiresApproval", approved_signature AS "approvedSignature",
         rejected_by AS "rejectedBy", rejected_at AS "rejectedAt", rejection_reason AS "rejectionReason",
         notes, created_at AS "createdAt", updated_at AS "updatedAt" 
         FROM employee_vacations WHERE employee_id = $1 ORDER BY start_date DESC`,
        [emp.id]
      );

      // Больничные
      const sickLeavesRows = await query(
        `SELECT id, employee_id AS "employeeId", start_date AS "startDate", end_date AS "endDate", days_count AS "daysCount", 
         sick_leave_number AS "sickLeaveNumber", diagnosis, medical_institution AS "medicalInstitution", 
         status, requires_approval AS "requiresApproval", approved_by AS "approvedBy", approved_at AS "approvedAt", approved_signature AS "approvedSignature",
         closed_at AS "closedAt", notes, file_url AS "fileUrl", expected_return_date AS "expectedReturnDate",
         created_at AS "createdAt", updated_at AS "updatedAt" 
         FROM employee_sick_leaves WHERE employee_id = $1 ORDER BY start_date DESC`,
        [emp.id]
      );

      // Увольнения
      const terminationsRows = await query(
        `SELECT id, employee_id AS "employeeId", termination_date AS "terminationDate", reason, initiated_by AS "initiatedBy", 
         initiated_at AS "initiatedAt", status, termination_contract_number AS "terminationContractNumber", 
         termination_contract_date AS "terminationContractDate", termination_contract_file_url AS "terminationContractFileUrl",
         final_settlement_amount AS "finalSettlementAmount", unused_vacation_days AS "unusedVacationDays", 
         compensation_amount AS "compensationAmount", severance_pay AS "severancePay", other_payments AS "otherPayments", 
         deductions, settlement_document_number AS "settlementDocumentNumber", settlement_document_date AS "settlementDocumentDate", 
         settlement_document_file_url AS "settlementDocumentFileUrl", notes, completed_at AS "completedAt", 
         created_at AS "createdAt", updated_at AS "updatedAt" 
         FROM employee_terminations WHERE employee_id = $1 ORDER BY termination_date DESC`,
        [emp.id]
      );

      // Отгулы и прогулы
      const absencesRows = await query(
        `SELECT id, employee_id AS "employeeId", absence_type AS "absenceType", start_date AS "startDate", end_date AS "endDate",
         start_time AS "startTime", end_time AS "endTime", days_count AS "daysCount", reason, requires_approval AS "requiresApproval",
         status, approved_by AS "approvedBy", approved_at AS "approvedAt", approved_signature AS "approvedSignature",
         rejected_by AS "rejectedBy", rejected_at AS "rejectedAt", rejection_reason AS "rejectionReason",
         notes, created_at AS "createdAt", updated_at AS "updatedAt"
         FROM employee_absences WHERE employee_id = $1 ORDER BY start_date DESC, created_at DESC`,
        [emp.id]
      );

      const hrData = {};
      if (passportRows.length > 0) {
        hrData.passportData = passportRows[0];
      }
      if (laborBookRows.length > 0) {
        hrData.laborBook = {
          ...laborBookRows[0],
          entries: laborBookEntries
        };
      }
      if (certificatesRows.length > 0) {
        hrData.certificates = certificatesRows;
      }
      if (otherDocsRows.length > 0) {
        hrData.otherDocuments = otherDocsRows;
      }
      if (accountingRows.length > 0) {
        hrData.accountingData = accountingRows[0];
      }
      if (contractsRows.length > 0) {
        hrData.contracts = contractsRows;
      }
      if (vacationsRows.length > 0) {
        hrData.vacations = vacationsRows;
      }
      if (sickLeavesRows.length > 0) {
        hrData.sickLeaves = sickLeavesRows;
      }
      if (terminationsRows.length > 0) {
        hrData.terminations = terminationsRows;
      }
      if (absencesRows.length > 0) {
        hrData.absences = absencesRows;
      }

      // Обрабатываем messengerLogins - может быть строкой JSON или массивом
      let messengerLogins = [];
      if (emp.messengerLogins) {
        if (typeof emp.messengerLogins === 'string') {
          try {
            messengerLogins = JSON.parse(emp.messengerLogins);
          } catch (e) {
            messengerLogins = [];
          }
        } else if (Array.isArray(emp.messengerLogins)) {
          messengerLogins = emp.messengerLogins;
        }
      }

      const assignedDistrictIds = districtIdsByEmployee[emp.id] && districtIdsByEmployee[emp.id].length > 0
        ? districtIdsByEmployee[emp.id]
        : (emp.assignedDistrictId ? [emp.assignedDistrictId] : []);
      return {
        ...emp,
        assignedDistrictIds,
        assignedDistrictId: assignedDistrictIds[0] || emp.assignedDistrictId || null,
        messengerLogins: messengerLogins,
        hrData: Object.keys(hrData).length > 0 ? hrData : undefined
      };
    }));

    res.json(employeesWithHrData);
  } catch (err) {
    console.error('Error fetching employees:', err);
    // #region agent log
    try {
      const debugLogPath = path.join(__dirname, '..', '.cursor', 'debug.log');
      fs.appendFileSync(debugLogPath, JSON.stringify({ location: 'server.js:GET/employees', message: 'Error fetching employees', data: { errMessage: err.message, errCode: err && err.code }, timestamp: Date.now(), sessionId: 'debug-session', hypothesisId: 'E' }) + '\n');
    } catch (_) {}
    // #endregion
    res.status(500).json({ error: 'Failed to fetch employees' });
  }
});

// GET /api/employees/organizational-structure -> организационная структура в виде дерева
app.get(`${API_PREFIX}/employees/organizational-structure`, async (req, res) => {
  try {
    const rows = await query(`
      SELECT 
        e.id,
        e.name,
        e.position,
        e.phone,
        e.status,
        e.photo_url AS "photoUrl",
        e.manager_id AS "managerId"
      FROM employees e
      WHERE e.status = 'active'
      ORDER BY e.name
    `);

    // Создаем карту всех сотрудников
    const employeesMap = new Map();
    rows.forEach(emp => {
      employeesMap.set(emp.id, {
        ...emp,
        subordinates: []
      });
    });

    // Строим дерево: находим корневые элементы (без manager_id) и добавляем подчиненных
    const rootEmployees = [];
    rows.forEach(emp => {
      const employee = employeesMap.get(emp.id);
      if (emp.managerId && employeesMap.has(emp.managerId)) {
        // Добавляем к подчиненным руководителя
        const manager = employeesMap.get(emp.managerId);
        manager.subordinates.push(employee);
      } else {
        // Это корневой элемент (нет руководителя или руководитель не найден)
        rootEmployees.push(employee);
      }
    });

    // Функция для рекурсивного подсчета подчиненных
    const countSubordinates = (emp) => {
      let count = emp.subordinates.length;
      emp.subordinates.forEach(sub => {
        count += countSubordinates(sub);
      });
      return count;
    };

    // Добавляем количество подчиненных к каждому сотруднику
    const addSubordinateCount = (emp) => {
      emp.subordinateCount = countSubordinates(emp);
      emp.subordinates.forEach(addSubordinateCount);
    };

    rootEmployees.forEach(addSubordinateCount);

    res.json(rootEmployees);
  } catch (err) {
    console.error('Error fetching organizational structure:', err);
    res.status(500).json({ error: 'Failed to fetch organizational structure' });
  }
});

// POST /api/employees -> создание нового сотрудника
app.post(`${API_PREFIX}/employees`, async (req, res) => {
  try {
    const { name, position, phone, status, salary, assignedDistrictId, assignedDistrictIds, managerId, birthDate, photoUrl, registrationDate, messengerLogins, hrData } = req.body;

    if (!name || !position || !phone || !salary) {
      return res.status(400).json({ error: 'name, position, phone и salary обязательны' });
    }

    const id = `e-${Date.now()}`;
    const employeeStatus = status || 'active';
    const districtIds = Array.isArray(assignedDistrictIds) ? assignedDistrictIds.filter(Boolean) : (assignedDistrictId ? [assignedDistrictId] : []);
    const firstDistrictId = districtIds.length > 0 ? districtIds[0] : (assignedDistrictId || null);

    // Создаем сотрудника
    await query(
      `INSERT INTO employees (id, name, position, phone, status, salary, assigned_district_id, manager_id, birth_date, photo_url, registration_date)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
      [id, name, position, phone, employeeStatus, salary, firstDistrictId, managerId || null, birthDate && birthDate.trim() !== '' ? birthDate : null, photoUrl || null, registrationDate && registrationDate.trim() !== '' ? registrationDate : null]
    );

    // Назначения на участки (многие ко многим)
    try {
      for (const districtId of districtIds) {
        if (districtId && String(districtId).trim()) {
          await query('INSERT INTO employee_districts (employee_id, district_id) VALUES ($1, $2) ON CONFLICT (employee_id, district_id) DO NOTHING', [id, String(districtId).trim()]);
        }
      }
    } catch (edErr) {
      // Таблица employee_districts может отсутствовать
    }

    // Добавляем логины мессенджеров
    if (messengerLogins && Array.isArray(messengerLogins)) {
      for (const login of messengerLogins) {
        if (login.messenger && login.login) {
          await query(
            'INSERT INTO employee_messenger_logins (employee_id, messenger, login) VALUES ($1, $2, $3)',
            [id, login.messenger, login.login]
          );
        }
      }
    }

    // Добавляем HR данные если есть
    if (hrData) {
      if (hrData.passportData) {
        const { series, number, issuedBy, issuedDate, registrationAddress } = hrData.passportData;
        // Проверяем, что все обязательные поля заполнены
        if (!series || !series.trim()) {
          return res.status(400).json({ error: 'Серия паспорта обязательна для заполнения' });
        }
        if (!number || !number.trim()) {
          return res.status(400).json({ error: 'Номер паспорта обязателен для заполнения' });
        }
        if (!issuedBy || !issuedBy.trim()) {
          return res.status(400).json({ error: 'Кем выдан паспорт - обязательное поле для заполнения' });
        }
        if (!issuedDate || !issuedDate.trim()) {
          return res.status(400).json({ error: 'Дата выдачи паспорта обязательна для заполнения' });
        }
        if (!registrationAddress || !registrationAddress.trim()) {
          return res.status(400).json({ error: 'Адрес регистрации обязателен для заполнения' });
        }
        await query(
          `INSERT INTO employee_passport_data (employee_id, series, number, issued_by, issued_date, registration_address)
           VALUES ($1, $2, $3, $4, $5, $6)`,
          [id, series.trim(), number.trim(), issuedBy.trim(), issuedDate.trim(), registrationAddress.trim()]
        );
      }

      if (hrData.laborBook) {
        const { number: lbNumber, series: lbSeries } = hrData.laborBook;
        const lbResult = await query(
          'INSERT INTO employee_labor_books (employee_id, number, series) VALUES ($1, $2, $3) RETURNING id',
          [id, lbNumber, lbSeries || null]
        );
        const lbId = lbResult[0].id;

        if (hrData.laborBook.entries && Array.isArray(hrData.laborBook.entries)) {
          for (const entry of hrData.laborBook.entries) {
            await query(
              'INSERT INTO labor_book_entries (labor_book_id, date, organization, position) VALUES ($1, $2, $3, $4)',
              [lbId, entry.date && entry.date.trim() !== '' ? entry.date : null, entry.organization, entry.position]
            );
          }
        }
      }

      // Бухгалтерская информация
      if (hrData.accountingData) {
        const { inn, snils, bankName, bankAccount, correspondentAccount, bik, taxId } = hrData.accountingData;
        await query(
          `INSERT INTO employee_accounting_data (employee_id, inn, snils, bank_name, bank_account, correspondent_account, bik, tax_id)
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
          [id, inn || null, snils || null, bankName || null, bankAccount || null, correspondentAccount || null, bik || null, taxId || null]
        );
      }

      // Договоры
      if (hrData.contracts && Array.isArray(hrData.contracts)) {
        for (const contract of hrData.contracts) {
          const { contractType, contractNumber, startDate, endDate, probationPeriodDays, workSchedule, workMode, contractTerms } = contract;
          await query(
            `INSERT INTO employee_contracts (employee_id, contract_type, contract_number, start_date, end_date, probation_period_days, work_schedule, work_mode, contract_terms)
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
            [id, contractType, contractNumber || null, startDate && startDate.trim() !== '' ? startDate : null, endDate && endDate.trim() !== '' ? endDate : null, probationPeriodDays || null, workSchedule || null, workMode || null, contractTerms || null]
          );
        }
      }
    }

    // Получаем созданного сотрудника с полными данными
    const result = await query(`
      SELECT 
        e.id,
        e.name,
        e.position,
        e.phone,
        e.status,
        e.salary,
        e.assigned_district_id AS "assignedDistrictId",
        e.birth_date AS "birthDate",
        e.photo_url AS "employeePhotoUrl",
        COALESCE(e.photo_url, pu.photo_url) AS "photoUrl",
        e.registration_date AS "registrationDate"
      FROM employees e
      LEFT JOIN portal_users pu ON e.id = pu.employee_id
      WHERE e.id = $1
    `, [id]);

    const messengerLoginsResult = await query(
      'SELECT messenger, login FROM employee_messenger_logins WHERE employee_id = $1',
      [id]
    );

    // Получаем HR данные
    const passportRows = await query(
      'SELECT series, number, issued_by AS "issuedBy", issued_date AS "issuedDate", registration_address AS "registrationAddress" FROM employee_passport_data WHERE employee_id = $1',
      [id]
    );
    const laborBookRows = await query(
      'SELECT number, series FROM employee_labor_books WHERE employee_id = $1',
      [id]
    );
    const laborBookEntries = await query(
      'SELECT date, organization, position FROM labor_book_entries WHERE labor_book_id = (SELECT id FROM employee_labor_books WHERE employee_id = $1) ORDER BY date',
      [id]
    );
    const accountingRows = await query(
      'SELECT inn, snils, bank_name AS "bankName", bank_account AS "bankAccount", correspondent_account AS "correspondentAccount", bik, tax_id AS "taxId" FROM employee_accounting_data WHERE employee_id = $1',
      [id]
    );
    const contractsRows = await query(
      'SELECT id, contract_type AS "contractType", contract_number AS "contractNumber", start_date AS "startDate", end_date AS "endDate", probation_period_days AS "probationPeriodDays", work_schedule AS "workSchedule", work_mode AS "workMode", contract_terms AS "contractTerms" FROM employee_contracts WHERE employee_id = $1 ORDER BY start_date DESC',
      [id]
    );

    const responseHrData = {};
    if (passportRows.length > 0) {
      responseHrData.passportData = passportRows[0];
    }
    if (laborBookRows.length > 0) {
      responseHrData.laborBook = {
        ...laborBookRows[0],
        entries: laborBookEntries
      };
    }
    if (accountingRows.length > 0) {
      responseHrData.accountingData = accountingRows[0];
    }
    if (contractsRows.length > 0) {
      responseHrData.contracts = contractsRows;
    }

    res.status(201).json({
      ...result[0],
      assignedDistrictIds: districtIds,
      assignedDistrictId: firstDistrictId,
      messengerLogins: messengerLoginsResult.map(ml => ({ messenger: ml.messenger, login: ml.login })),
      hrData: Object.keys(responseHrData).length > 0 ? responseHrData : undefined
    });
  } catch (err) {
    console.error('Error creating employee:', err);
    res.status(500).json({ error: 'Failed to create employee' });
  }
});

// PUT /api/employees/:id -> обновление сотрудника
app.put(`${API_PREFIX}/employees/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { name, position, phone, status, salary, assignedDistrictId, assignedDistrictIds, managerId, birthDate, photoUrl, registrationDate, messengerLogins, hrData } = req.body;

    // Проверяем существование сотрудника
    const existing = await query('SELECT id FROM employees WHERE id = $1', [id]);
    if (existing.length === 0) {
      return res.status(404).json({ error: 'Сотрудник не найден' });
    }

    // Нормализуем участки: массив или строка (например "id1,id2") из формы
    let districtIds = undefined;
    if (assignedDistrictIds !== undefined) {
      if (Array.isArray(assignedDistrictIds)) {
        districtIds = assignedDistrictIds.filter(Boolean).map((d) => String(d).trim()).filter(Boolean);
      } else if (typeof assignedDistrictIds === 'string' && assignedDistrictIds.trim()) {
        districtIds = assignedDistrictIds.split(/[,;\s]+/).map((d) => d.trim()).filter(Boolean);
      } else {
        districtIds = [];
      }
    }
    const firstDistrictId = districtIds !== undefined ? (districtIds.length > 0 ? districtIds[0] : null) : (assignedDistrictId !== undefined ? (assignedDistrictId || null) : undefined);

    // Обновляем основные данные
    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (name !== undefined) {
      updateFields.push(`name = $${paramIndex++}`);
      values.push(name);
    }
    if (position !== undefined) {
      updateFields.push(`position = $${paramIndex++}`);
      values.push(position);
    }
    if (phone !== undefined) {
      updateFields.push(`phone = $${paramIndex++}`);
      values.push(phone);
    }
    if (status !== undefined) {
      updateFields.push(`status = $${paramIndex++}`);
      values.push(status);
    }
    if (salary !== undefined) {
      updateFields.push(`salary = $${paramIndex++}`);
      values.push(salary);
    }
    if (firstDistrictId !== undefined) {
      updateFields.push(`assigned_district_id = $${paramIndex++}`);
      values.push(firstDistrictId);
    } else if (assignedDistrictId !== undefined) {
      updateFields.push(`assigned_district_id = $${paramIndex++}`);
      values.push(assignedDistrictId || null);
    }
    if (managerId !== undefined) {
      updateFields.push(`manager_id = $${paramIndex++}`);
      values.push(managerId || null);
    }
    if (birthDate !== undefined) {
      updateFields.push(`birth_date = $${paramIndex++}`);
      values.push(birthDate && birthDate.trim() !== '' ? birthDate : null);
    }
    if (photoUrl !== undefined) {
      updateFields.push(`photo_url = $${paramIndex++}`);
      values.push(photoUrl || null);
    }
    if (registrationDate !== undefined) {
      updateFields.push(`registration_date = $${paramIndex++}`);
      values.push(registrationDate && registrationDate.trim() !== '' ? registrationDate : null);
    }

    if (updateFields.length > 0) {
      updateFields.push(`updated_at = NOW()`);
      values.push(id);
      await query(
        `UPDATE employees SET ${updateFields.join(', ')} WHERE id = $${paramIndex}`,
        values
      );
    }

    // Назначения на участки (многие ко многим): при передаче assignedDistrictIds перезаписываем
    if (districtIds !== undefined) {
      try {
        await query('DELETE FROM employee_districts WHERE employee_id = $1', [id]);
        for (const districtId of districtIds) {
          if (districtId && String(districtId).trim()) {
            await query('INSERT INTO employee_districts (employee_id, district_id) VALUES ($1, $2) ON CONFLICT (employee_id, district_id) DO NOTHING', [id, String(districtId).trim()]);
          }
        }
      } catch (edErr) {
        console.error('[PUT /api/employees/:id] employee_districts update failed:', edErr.message);
      }
    }

    // Обновляем логины мессенджеров
    if (messengerLogins !== undefined) {
      // Удаляем старые
      await query('DELETE FROM employee_messenger_logins WHERE employee_id = $1', [id]);
      // Добавляем новые
      if (Array.isArray(messengerLogins)) {
        for (const login of messengerLogins) {
          if (login.messenger && login.login) {
            await query(
              'INSERT INTO employee_messenger_logins (employee_id, messenger, login) VALUES ($1, $2, $3)',
              [id, login.messenger, login.login]
            );
          }
        }
      }
    }

    // Обновляем HR данные
    if (hrData !== undefined) {
      if (hrData.passportData) {
        const { series, number, issuedBy, issuedDate, registrationAddress } = hrData.passportData;
        // Проверяем, что все обязательные поля заполнены
        if (!series || !series.trim()) {
          return res.status(400).json({ error: 'Серия паспорта обязательна для заполнения' });
        }
        if (!number || !number.trim()) {
          return res.status(400).json({ error: 'Номер паспорта обязателен для заполнения' });
        }
        if (!issuedBy || !issuedBy.trim()) {
          return res.status(400).json({ error: 'Кем выдан паспорт - обязательное поле для заполнения' });
        }
        if (!issuedDate || !issuedDate.trim()) {
          return res.status(400).json({ error: 'Дата выдачи паспорта обязательна для заполнения' });
        }
        if (!registrationAddress || !registrationAddress.trim()) {
          return res.status(400).json({ error: 'Адрес регистрации обязателен для заполнения' });
        }
        // Удаляем старые данные
        await query('DELETE FROM employee_passport_data WHERE employee_id = $1', [id]);
        // Добавляем новые
        await query(
          `INSERT INTO employee_passport_data (employee_id, series, number, issued_by, issued_date, registration_address)
           VALUES ($1, $2, $3, $4, $5, $6)`,
          [id, series.trim(), number.trim(), issuedBy.trim(), issuedDate.trim(), registrationAddress.trim()]
        );
      }

      if (hrData.laborBook) {
        // Удаляем старые записи
        const oldLb = await query('SELECT id FROM employee_labor_books WHERE employee_id = $1', [id]);
        if (oldLb.length > 0) {
          await query('DELETE FROM labor_book_entries WHERE labor_book_id = $1', [oldLb[0].id]);
          await query('DELETE FROM employee_labor_books WHERE employee_id = $1', [id]);
        }

        // Добавляем новые
        const { number: lbNumber, series: lbSeries } = hrData.laborBook;
        const lbResult = await query(
          'INSERT INTO employee_labor_books (employee_id, number, series) VALUES ($1, $2, $3) RETURNING id',
          [id, lbNumber, lbSeries || null]
        );
        const lbId = lbResult[0].id;

        if (hrData.laborBook.entries && Array.isArray(hrData.laborBook.entries)) {
          for (const entry of hrData.laborBook.entries) {
            await query(
              'INSERT INTO labor_book_entries (labor_book_id, date, organization, position) VALUES ($1, $2, $3, $4)',
              [lbId, entry.date && entry.date.trim() !== '' ? entry.date : null, entry.organization, entry.position]
            );
          }
        }
      }

      // Бухгалтерская информация
      if (hrData.accountingData !== undefined) {
        // Удаляем старые данные
        await query('DELETE FROM employee_accounting_data WHERE employee_id = $1', [id]);
        // Добавляем новые, если есть данные
        const { inn, snils, bankName, bankAccount, correspondentAccount, bik, taxId } = hrData.accountingData || {};
        if (inn || snils || bankName || bankAccount || correspondentAccount || bik || taxId) {
          await query(
            `INSERT INTO employee_accounting_data (employee_id, inn, snils, bank_name, bank_account, correspondent_account, bik, tax_id)
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
            [id, inn || null, snils || null, bankName || null, bankAccount || null, correspondentAccount || null, bik || null, taxId || null]
          );
        }
      }

      // Договоры
      if (hrData.contracts !== undefined) {
        // Удаляем старые договоры
        await query('DELETE FROM employee_contracts WHERE employee_id = $1', [id]);
        // Добавляем новые, если есть
        if (Array.isArray(hrData.contracts) && hrData.contracts.length > 0) {
          for (const contract of hrData.contracts) {
            const { contractType, contractNumber, startDate, endDate, probationPeriodDays, workSchedule, workMode, contractTerms } = contract;
            await query(
              `INSERT INTO employee_contracts (employee_id, contract_type, contract_number, start_date, end_date, probation_period_days, work_schedule, work_mode, contract_terms)
               VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
              [id, contractType, contractNumber || null, startDate && startDate.trim() !== '' ? startDate : null, endDate && endDate.trim() !== '' ? endDate : null, probationPeriodDays || null, workSchedule || null, workMode || null, contractTerms || null]
            );
          }
        }
      }
    }

    // Получаем обновленного сотрудника с полными данными
    const result = await query(`
      SELECT 
        e.id,
        e.name,
        e.position,
        e.phone,
        e.status,
        e.salary,
        e.assigned_district_id AS "assignedDistrictId",
        e.birth_date AS "birthDate",
        e.photo_url AS "employeePhotoUrl",
        COALESCE(e.photo_url, pu.photo_url) AS "photoUrl",
        e.registration_date AS "registrationDate"
      FROM employees e
      LEFT JOIN portal_users pu ON e.id = pu.employee_id
      WHERE e.id = $1
    `, [id]);

    const messengerLoginsResult = await query(
      'SELECT messenger, login FROM employee_messenger_logins WHERE employee_id = $1',
      [id]
    );

    // Получаем HR данные
    const passportRows = await query(
      'SELECT series, number, issued_by AS "issuedBy", issued_date AS "issuedDate", registration_address AS "registrationAddress" FROM employee_passport_data WHERE employee_id = $1',
      [id]
    );
    const laborBookRows = await query(
      'SELECT number, series FROM employee_labor_books WHERE employee_id = $1',
      [id]
    );
    const laborBookEntries = await query(
      'SELECT date, organization, position FROM labor_book_entries WHERE labor_book_id = (SELECT id FROM employee_labor_books WHERE employee_id = $1) ORDER BY date',
      [id]
    );
    const accountingRows = await query(
      'SELECT inn, snils, bank_name AS "bankName", bank_account AS "bankAccount", correspondent_account AS "correspondentAccount", bik, tax_id AS "taxId" FROM employee_accounting_data WHERE employee_id = $1',
      [id]
    );
    const contractsRows = await query(
      'SELECT id, contract_type AS "contractType", contract_number AS "contractNumber", start_date AS "startDate", end_date AS "endDate", probation_period_days AS "probationPeriodDays", work_schedule AS "workSchedule", work_mode AS "workMode", contract_terms AS "contractTerms" FROM employee_contracts WHERE employee_id = $1 ORDER BY start_date DESC',
      [id]
    );

    // Отпуска
    const vacationsRows = await query(
      `SELECT id, employee_id AS "employeeId", start_date AS "startDate", end_date AS "endDate", days_count AS "daysCount", 
       vacation_type AS "vacationType", status, approved_by AS "approvedBy", approved_at AS "approvedAt", 
       requires_approval AS "requiresApproval", approved_signature AS "approvedSignature",
       rejected_by AS "rejectedBy", rejected_at AS "rejectedAt", rejection_reason AS "rejectionReason",
       notes, created_at AS "createdAt", updated_at AS "updatedAt" 
       FROM employee_vacations WHERE employee_id = $1 ORDER BY start_date DESC`,
      [id]
    );

    // Больничные
    const sickLeavesRows = await query(
      `SELECT id, employee_id AS "employeeId", start_date AS "startDate", end_date AS "endDate", days_count AS "daysCount", 
       sick_leave_number AS "sickLeaveNumber", diagnosis, medical_institution AS "medicalInstitution", 
       status, requires_approval AS "requiresApproval", approved_by AS "approvedBy", approved_at AS "approvedAt", approved_signature AS "approvedSignature",
       closed_at AS "closedAt", notes, file_url AS "fileUrl", expected_return_date AS "expectedReturnDate",
       created_at AS "createdAt", updated_at AS "updatedAt" 
       FROM employee_sick_leaves WHERE employee_id = $1 ORDER BY start_date DESC`,
      [id]
    );

    // Увольнения
    const terminationsRows = await query(
      `SELECT id, employee_id AS "employeeId", termination_date AS "terminationDate", reason, initiated_by AS "initiatedBy", 
       initiated_at AS "initiatedAt", status, termination_contract_number AS "terminationContractNumber", 
       termination_contract_date AS "terminationContractDate", termination_contract_file_url AS "terminationContractFileUrl",
       final_settlement_amount AS "finalSettlementAmount", unused_vacation_days AS "unusedVacationDays", 
       compensation_amount AS "compensationAmount", severance_pay AS "severancePay", other_payments AS "otherPayments", 
       deductions, settlement_document_number AS "settlementDocumentNumber", settlement_document_date AS "settlementDocumentDate", 
       settlement_document_file_url AS "settlementDocumentFileUrl", notes, completed_at AS "completedAt", 
       created_at AS "createdAt", updated_at AS "updatedAt" 
       FROM employee_terminations WHERE employee_id = $1 ORDER BY termination_date DESC`,
      [id]
    );

    // Отгулы и прогулы
    const absencesRows = await query(
      `SELECT id, employee_id AS "employeeId", absence_type AS "absenceType", start_date AS "startDate", end_date AS "endDate",
       start_time AS "startTime", end_time AS "endTime", days_count AS "daysCount", reason, requires_approval AS "requiresApproval",
       status, approved_by AS "approvedBy", approved_at AS "approvedAt", approved_signature AS "approvedSignature",
       rejected_by AS "rejectedBy", rejected_at AS "rejectedAt", rejection_reason AS "rejectionReason",
       notes, created_at AS "createdAt", updated_at AS "updatedAt"
       FROM employee_absences WHERE employee_id = $1 ORDER BY start_date DESC, created_at DESC`,
      [id]
    );

    const responseHrData = {};
    if (passportRows.length > 0) {
      responseHrData.passportData = passportRows[0];
    }
    if (laborBookRows.length > 0) {
      responseHrData.laborBook = {
        ...laborBookRows[0],
        entries: laborBookEntries
      };
    }
    if (accountingRows.length > 0) {
      responseHrData.accountingData = accountingRows[0];
    }
    if (contractsRows.length > 0) {
      responseHrData.contracts = contractsRows;
    }
    if (vacationsRows.length > 0) {
      responseHrData.vacations = vacationsRows;
    }
    if (sickLeavesRows.length > 0) {
      responseHrData.sickLeaves = sickLeavesRows;
    }
    if (terminationsRows.length > 0) {
      responseHrData.terminations = terminationsRows;
    }
    if (absencesRows.length > 0) {
      responseHrData.absences = absencesRows;
    }

    let responseDistrictIds = [];
    try {
      const edRows = await query('SELECT district_id AS "districtId" FROM employee_districts WHERE employee_id = $1 ORDER BY district_id', [id]);
      responseDistrictIds = edRows.map(r => r.districtId);
    } catch (_) {}
    if (responseDistrictIds.length === 0 && result[0].assignedDistrictId) {
      responseDistrictIds = [result[0].assignedDistrictId];
    }
    if (responseDistrictIds.length === 0 && districtIds && districtIds.length > 0) {
      responseDistrictIds = districtIds;
    }

    res.json({
      ...result[0],
      assignedDistrictIds: responseDistrictIds,
      assignedDistrictId: responseDistrictIds[0] || result[0].assignedDistrictId || null,
      messengerLogins: messengerLoginsResult.map(ml => ({ messenger: ml.messenger, login: ml.login })),
      hrData: Object.keys(responseHrData).length > 0 ? responseHrData : undefined
    });
  } catch (err) {
    console.error('Error updating employee:', err);
    res.status(500).json({ error: 'Failed to update employee' });
  }
});

// DELETE /api/employees/:id -> удаление сотрудника (каскадно удалит portal_users и связанные HR-данные)
app.delete(`${API_PREFIX}/employees/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM employees WHERE id = $1 RETURNING id', [id]);
    if (result.length === 0) {
      return res.status(404).json({ error: 'Сотрудник не найден' });
    }
    res.json({ success: true, message: 'Сотрудник удалён' });
  } catch (err) {
    console.error('Error deleting employee:', err);
    res.status(500).json({ error: 'Ошибка при удалении сотрудника' });
  }
});

// ========= ОТПУСКА =========

// GET /api/employees/:id/vacations -> список отпусков сотрудника
app.get(`${API_PREFIX}/employees/:id/vacations`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      `SELECT id, employee_id AS "employeeId", start_date AS "startDate", end_date AS "endDate", days_count AS "daysCount", 
       vacation_type AS "vacationType", status, approved_by AS "approvedBy", approved_at AS "approvedAt", 
       requires_approval AS "requiresApproval", approved_signature AS "approvedSignature",
       rejected_by AS "rejectedBy", rejected_at AS "rejectedAt", rejection_reason AS "rejectionReason",
       notes, created_at AS "createdAt", updated_at AS "updatedAt" 
       FROM employee_vacations WHERE employee_id = $1 ORDER BY start_date DESC`,
      [id]
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching vacations:', err);
    res.status(500).json({ error: 'Failed to fetch vacations' });
  }
});

// POST /api/employees/:id/vacations -> создание отпуска
app.post(`${API_PREFIX}/employees/:id/vacations`, async (req, res) => {
  try {
    const { id } = req.params;
    const { startDate, endDate, daysCount, vacationType, status, approvedBy, notes, currentUserId, currentUserRole } = req.body;

    // Права: отпуск может оформить сотрудник себе, руководитель подчинённым, HR всем
    if (currentUserId && currentUserRole) {
      const isSelf = currentUserId === id;
      const isHrOrDirector = currentUserRole === 'HR_MANAGER' || currentUserRole === 'DIRECTOR';
      let isManager = false;
      if (!isSelf && !isHrOrDirector) {
        const empRow = await query('SELECT manager_id FROM employees WHERE id = $1', [id]);
        if (empRow.length > 0 && empRow[0].manager_id === currentUserId) isManager = true;
      }
      if (!isSelf && !isHrOrDirector && !isManager) {
        return res.status(403).json({ error: 'Недостаточно прав для создания отпуска для этого сотрудника' });
      }
    }

    if (!startDate || !endDate) {
      return res.status(400).json({ error: 'startDate и endDate обязательны' });
    }

    const calculatedDays = daysCount || Math.ceil((new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24)) + 1;

    const { requiresApproval, approvedSignature, rejectedBy, rejectedAt, rejectionReason } = req.body;

    const result = await query(
      `INSERT INTO employee_vacations (employee_id, start_date, end_date, days_count, vacation_type, status, approved_by, approved_at, requires_approval, approved_signature, rejected_by, rejected_at, rejection_reason, notes)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING 
       id, employee_id AS "employeeId", start_date AS "startDate", end_date AS "endDate", days_count AS "daysCount", 
       vacation_type AS "vacationType", status, approved_by AS "approvedBy", approved_at AS "approvedAt", 
       requires_approval AS "requiresApproval", approved_signature AS "approvedSignature",
       rejected_by AS "rejectedBy", rejected_at AS "rejectedAt", rejection_reason AS "rejectionReason",
       notes, created_at AS "createdAt", updated_at AS "updatedAt"`,
      [
        id, startDate, endDate, calculatedDays, vacationType || 'annual', status || 'planned', 
        approvedBy || null, approvedBy ? new Date().toISOString() : null,
        requiresApproval !== undefined ? requiresApproval : true,
        approvedSignature || null,
        rejectedBy || null, rejectedAt || null, rejectionReason || null,
        notes || null
      ]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating vacation:', err);
    res.status(500).json({ error: 'Failed to create vacation' });
  }
});

// PUT /api/employees/:id/vacations/:vacationId -> обновление отпуска
app.put(`${API_PREFIX}/employees/:id/vacations/:vacationId`, async (req, res) => {
  try {
    const { id, vacationId } = req.params;
    const { startDate, endDate, daysCount, vacationType, status, approvedBy, notes, rejectedBy, rejectionReason } = req.body;

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (startDate !== undefined) {
      updateFields.push(`start_date = $${paramIndex++}`);
      values.push(startDate);
    }
    if (endDate !== undefined) {
      updateFields.push(`end_date = $${paramIndex++}`);
      values.push(endDate);
    }
    if (daysCount !== undefined) {
      updateFields.push(`days_count = $${paramIndex++}`);
      values.push(daysCount);
    }
    if (vacationType !== undefined) {
      updateFields.push(`vacation_type = $${paramIndex++}`);
      values.push(vacationType);
    }
    if (status !== undefined) {
      updateFields.push(`status = $${paramIndex++}`);
      values.push(status);
      if (status === 'approved' && approvedBy) {
        updateFields.push(`approved_by = $${paramIndex++}`);
        values.push(approvedBy);
        updateFields.push(`approved_at = NOW()`);
      }
      if (status === 'rejected' && rejectedBy) {
        updateFields.push(`rejected_by = $${paramIndex++}`);
        values.push(rejectedBy);
        updateFields.push(`rejected_at = NOW()`);
        if (rejectionReason !== undefined) {
          updateFields.push(`rejection_reason = $${paramIndex++}`);
          values.push(rejectionReason);
        }
      }
    }
    if (notes !== undefined) {
      updateFields.push(`notes = $${paramIndex++}`);
      values.push(notes);
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`);
    values.push(id, vacationId);

    const result = await query(
      `UPDATE employee_vacations SET ${updateFields.join(', ')} 
       WHERE employee_id = $${paramIndex++} AND id = $${paramIndex} RETURNING 
       id, employee_id AS "employeeId", start_date AS "startDate", end_date AS "endDate", days_count AS "daysCount", 
       vacation_type AS "vacationType", status, approved_by AS "approvedBy", approved_at AS "approvedAt", 
       notes, created_at AS "createdAt", updated_at AS "updatedAt"`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Отпуск не найден' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating vacation:', err);
    res.status(500).json({ error: 'Failed to update vacation' });
  }
});

// DELETE /api/employees/:id/vacations/:vacationId -> удаление отпуска
app.delete(`${API_PREFIX}/employees/:id/vacations/:vacationId`, async (req, res) => {
  try {
    const { id, vacationId } = req.params;
    const result = await query(
      'DELETE FROM employee_vacations WHERE employee_id = $1 AND id = $2 RETURNING id',
      [id, vacationId]
    );
    if (result.length === 0) {
      return res.status(404).json({ error: 'Отпуск не найден' });
    }
    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting vacation:', err);
    res.status(500).json({ error: 'Failed to delete vacation' });
  }
});

// ========= БОЛЬНИЧНЫЕ =========

// GET /api/employees/:id/sick-leaves -> список больничных сотрудника
app.get(`${API_PREFIX}/employees/:id/sick-leaves`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      `SELECT id, employee_id AS "employeeId", start_date AS "startDate", end_date AS "endDate", days_count AS "daysCount", 
       sick_leave_number AS "sickLeaveNumber", diagnosis, medical_institution AS "medicalInstitution", 
       status, requires_approval AS "requiresApproval", approved_by AS "approvedBy", approved_at AS "approvedAt", approved_signature AS "approvedSignature",
       closed_at AS "closedAt", notes, file_url AS "fileUrl", expected_return_date AS "expectedReturnDate",
       created_at AS "createdAt", updated_at AS "updatedAt" 
       FROM employee_sick_leaves WHERE employee_id = $1 ORDER BY start_date DESC`,
      [id]
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching sick leaves:', err);
    res.status(500).json({ error: 'Failed to fetch sick leaves' });
  }
});

// POST /api/employees/:id/sick-leaves -> создание больничного
app.post(`${API_PREFIX}/employees/:id/sick-leaves`, async (req, res) => {
  try {
    const { id } = req.params;
    const { startDate, endDate, sickLeaveNumber, diagnosis, medicalInstitution, notes, fileUrl, expectedReturnDate, currentUserId, currentUserRole } = req.body;

    // Права: больничный может оформить сотрудник себе, руководитель подчинённым, HR всем
    if (currentUserId && currentUserRole) {
      const isSelf = currentUserId === id;
      const isHrOrDirector = currentUserRole === 'HR_MANAGER' || currentUserRole === 'DIRECTOR';
      let isManager = false;
      if (!isSelf && !isHrOrDirector) {
        const empRow = await query('SELECT manager_id FROM employees WHERE id = $1', [id]);
        if (empRow.length > 0 && empRow[0].manager_id === currentUserId) isManager = true;
      }
      if (!isSelf && !isHrOrDirector && !isManager) {
        return res.status(403).json({ error: 'Недостаточно прав для создания больничного для этого сотрудника' });
      }
    }

    if (!startDate) {
      return res.status(400).json({ error: 'startDate обязателен' });
    }

    let calculatedDays = null;
    if (endDate) {
      calculatedDays = Math.ceil((new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24)) + 1;
    }

    const { requiresApproval, approvedBy, approvedAt, approvedSignature } = req.body;

    const result = await query(
      `INSERT INTO employee_sick_leaves (employee_id, start_date, end_date, days_count, sick_leave_number, diagnosis, medical_institution, status, requires_approval, approved_by, approved_at, approved_signature, notes, file_url, expected_return_date)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING 
       id, employee_id AS "employeeId", start_date AS "startDate", end_date AS "endDate", days_count AS "daysCount", 
       sick_leave_number AS "sickLeaveNumber", diagnosis, medical_institution AS "medicalInstitution", 
       status, requires_approval AS "requiresApproval", approved_by AS "approvedBy", approved_at AS "approvedAt", approved_signature AS "approvedSignature",
       closed_at AS "closedAt", notes, file_url AS "fileUrl", expected_return_date AS "expectedReturnDate",
       created_at AS "createdAt", updated_at AS "updatedAt"`,
      [
        id, startDate, endDate || null, calculatedDays, sickLeaveNumber || null, diagnosis || null, medicalInstitution || null,
        'active', requiresApproval !== undefined ? requiresApproval : false,
        approvedBy || null, approvedAt || null, approvedSignature || null,
        notes || null, fileUrl || null, expectedReturnDate || null
      ]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating sick leave:', err);
    res.status(500).json({ error: 'Failed to create sick leave' });
  }
});

// PUT /api/employees/:id/sick-leaves/:sickLeaveId -> обновление больничного
app.put(`${API_PREFIX}/employees/:id/sick-leaves/:sickLeaveId`, async (req, res) => {
  try {
    const { id, sickLeaveId } = req.params;
    const { startDate, endDate, daysCount, sickLeaveNumber, diagnosis, medicalInstitution, status, notes, fileUrl, expectedReturnDate } = req.body;

    // При закрытии больничного номер больничного обязателен
    if (status === 'closed') {
      const current = await query('SELECT sick_leave_number FROM employee_sick_leaves WHERE id = $1 AND employee_id = $2', [sickLeaveId, id]);
      const hasNumber = (sickLeaveNumber && sickLeaveNumber.trim()) || (current[0] && current[0].sick_leave_number);
      if (!hasNumber) {
        return res.status(400).json({ error: 'При выходе с больничного номер больничного листа обязателен' });
      }
    }

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (startDate !== undefined) {
      updateFields.push(`start_date = $${paramIndex++}`);
      values.push(startDate);
    }
    if (endDate !== undefined) {
      updateFields.push(`end_date = $${paramIndex++}`);
      values.push(endDate);
      const current = await query('SELECT start_date FROM employee_sick_leaves WHERE id = $1', [sickLeaveId]);
      const start = startDate !== undefined ? startDate : current[0]?.start_date;
      const end = endDate;
      if (start && end) {
        const calculatedDays = Math.ceil((new Date(end) - new Date(start)) / (1000 * 60 * 60 * 24)) + 1;
        updateFields.push(`days_count = $${paramIndex++}`);
        values.push(calculatedDays);
      }
    }
    if (daysCount !== undefined && endDate === undefined) {
      updateFields.push(`days_count = $${paramIndex++}`);
      values.push(daysCount);
    }
    if (sickLeaveNumber !== undefined) {
      updateFields.push(`sick_leave_number = $${paramIndex++}`);
      values.push(sickLeaveNumber);
    }
    if (diagnosis !== undefined) {
      updateFields.push(`diagnosis = $${paramIndex++}`);
      values.push(diagnosis);
    }
    if (medicalInstitution !== undefined) {
      updateFields.push(`medical_institution = $${paramIndex++}`);
      values.push(medicalInstitution);
    }
    if (expectedReturnDate !== undefined) {
      updateFields.push(`expected_return_date = $${paramIndex++}`);
      values.push(expectedReturnDate || null);
    }
    if (status !== undefined) {
      updateFields.push(`status = $${paramIndex++}`);
      values.push(status);
      if (status === 'closed') {
        updateFields.push(`closed_at = NOW()`);
      }
    }
    if (notes !== undefined) {
      updateFields.push(`notes = $${paramIndex++}`);
      values.push(notes);
    }
    if (fileUrl !== undefined) {
      updateFields.push(`file_url = $${paramIndex++}`);
      values.push(fileUrl);
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`);
    values.push(id, sickLeaveId);

    const result = await query(
      `UPDATE employee_sick_leaves SET ${updateFields.join(', ')} 
       WHERE employee_id = $${paramIndex++} AND id = $${paramIndex} RETURNING 
       id, employee_id AS "employeeId", start_date AS "startDate", end_date AS "endDate", days_count AS "daysCount", 
       sick_leave_number AS "sickLeaveNumber", diagnosis, medical_institution AS "medicalInstitution", 
       status, closed_at AS "closedAt", notes, file_url AS "fileUrl", expected_return_date AS "expectedReturnDate",
       created_at AS "createdAt", updated_at AS "updatedAt"`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Больничный не найден' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating sick leave:', err);
    res.status(500).json({ error: 'Failed to update sick leave' });
  }
});

// DELETE /api/employees/:id/sick-leaves/:sickLeaveId -> удаление больничного
app.delete(`${API_PREFIX}/employees/:id/sick-leaves/:sickLeaveId`, async (req, res) => {
  try {
    const { id, sickLeaveId } = req.params;
    const result = await query(
      'DELETE FROM employee_sick_leaves WHERE employee_id = $1 AND id = $2 RETURNING id',
      [id, sickLeaveId]
    );
    if (result.length === 0) {
      return res.status(404).json({ error: 'Больничный не найден' });
    }
    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting sick leave:', err);
    res.status(500).json({ error: 'Failed to delete sick leave' });
  }
});

// ========= ОТГУЛЫ И ПРОГУЛЫ =========

// GET /api/employees/:id/absences -> список отгулов и прогулов сотрудника
app.get(`${API_PREFIX}/employees/:id/absences`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      `SELECT id, employee_id AS "employeeId", absence_type AS "absenceType", start_date AS "startDate", end_date AS "endDate",
       start_time AS "startTime", end_time AS "endTime", days_count AS "daysCount", reason, requires_approval AS "requiresApproval",
       status, approved_by AS "approvedBy", approved_at AS "approvedAt", approved_signature AS "approvedSignature",
       rejected_by AS "rejectedBy", rejected_at AS "rejectedAt", rejection_reason AS "rejectionReason",
       notes, created_at AS "createdAt", updated_at AS "updatedAt"
       FROM employee_absences WHERE employee_id = $1 ORDER BY start_date DESC, created_at DESC`,
      [id]
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching absences:', err);
    res.status(500).json({ error: 'Failed to fetch absences' });
  }
});

// POST /api/employees/:id/absences -> создание отгула/прогула
app.post(`${API_PREFIX}/employees/:id/absences`, async (req, res) => {
  try {
    const { id } = req.params;
    const { 
      absenceType, 
      startDate, 
      endDate, 
      startTime, 
      endTime, 
      daysCount, 
      reason, 
      requiresApproval, 
      status,
      approvedBy,
      approvedAt,
      approvedSignature,
      notes,
      currentUserId, // ID текущего пользователя (сотрудника)
      currentUserRole // Роль текущего пользователя (DIRECTOR, HR_MANAGER, ENGINEER, MASTER и т.д.)
    } = req.body;

    if (!startDate) {
      return res.status(400).json({ error: 'startDate обязателен' });
    }

    if (!absenceType || !['day_off', 'absence', 'late', 'early_leave'].includes(absenceType)) {
      return res.status(400).json({ error: 'Некорректный тип отсутствия' });
    }

    // Проверка прав доступа:
    // Отгул (day_off): сотрудник себе, руководитель подчинённым, HR всем.
    // Прогул/опоздание/ранний уход (absence, late, early_leave): только руководитель подчинённым или HR всем.
    if (currentUserId && currentUserRole) {
      const isSelf = currentUserId === id;
      const isHrOrDirector = currentUserRole === 'HR_MANAGER' || currentUserRole === 'DIRECTOR';

      let isManager = false;
      if (!isSelf && !isHrOrDirector) {
        const employeeResult = await query(
          'SELECT manager_id FROM employees WHERE id = $1',
          [id]
        );
        if (employeeResult.length > 0 && employeeResult[0].manager_id === currentUserId) {
          isManager = true;
        }
      }

      if (!isSelf && !isHrOrDirector && !isManager) {
        return res.status(403).json({ error: 'Недостаточно прав для создания отгула/пропуска для этого сотрудника' });
      }

      // Прогул, опоздание, ранний уход — только руководитель или HR, не сам себе
      const onlyManagerOrHr = ['absence', 'late', 'early_leave'].includes(absenceType);
      if (onlyManagerOrHr && isSelf) {
        return res.status(403).json({ error: 'Прогул, опоздание и ранний уход может оформить только руководитель или HR' });
      }
    }

    const result = await query(
      `INSERT INTO employee_absences (employee_id, absence_type, start_date, end_date, start_time, end_time, days_count, reason, requires_approval, status, approved_by, approved_at, approved_signature, notes)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING 
       id, employee_id AS "employeeId", absence_type AS "absenceType", start_date AS "startDate", end_date AS "endDate",
       start_time AS "startTime", end_time AS "endTime", days_count AS "daysCount", reason, requires_approval AS "requiresApproval",
       status, approved_by AS "approvedBy", approved_at AS "approvedAt", approved_signature AS "approvedSignature",
       rejected_by AS "rejectedBy", rejected_at AS "rejectedAt", rejection_reason AS "rejectionReason",
       notes, created_at AS "createdAt", updated_at AS "updatedAt"`,
      [
        id, 
        absenceType, 
        startDate, 
        endDate || null, 
        startTime || null, 
        endTime || null, 
        daysCount || 1.0, 
        reason || null, 
        requiresApproval !== undefined ? requiresApproval : true,
        status || 'pending',
        approvedBy || null,
        approvedAt || null,
        approvedSignature || null,
        notes || null
      ]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating absence:', err);
    res.status(500).json({ error: 'Failed to create absence' });
  }
});

// PUT /api/employees/:id/absences/:absenceId -> обновление отгула/прогула
app.put(`${API_PREFIX}/employees/:id/absences/:absenceId`, async (req, res) => {
  try {
    const { id, absenceId } = req.params;
    const { 
      absenceType, 
      startDate, 
      endDate, 
      startTime, 
      endTime, 
      daysCount, 
      reason, 
      requiresApproval, 
      status,
      approvedBy,
      approvedAt,
      approvedSignature,
      rejectedBy,
      rejectedAt,
      rejectionReason,
      notes 
    } = req.body;

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (absenceType !== undefined) {
      updateFields.push(`absence_type = $${paramIndex++}`);
      values.push(absenceType);
    }
    if (startDate !== undefined) {
      updateFields.push(`start_date = $${paramIndex++}`);
      values.push(startDate);
    }
    if (endDate !== undefined) {
      updateFields.push(`end_date = $${paramIndex++}`);
      values.push(endDate);
    }
    if (startTime !== undefined) {
      updateFields.push(`start_time = $${paramIndex++}`);
      values.push(startTime);
    }
    if (endTime !== undefined) {
      updateFields.push(`end_time = $${paramIndex++}`);
      values.push(endTime);
    }
    if (daysCount !== undefined) {
      updateFields.push(`days_count = $${paramIndex++}`);
      values.push(daysCount);
    }
    if (reason !== undefined) {
      updateFields.push(`reason = $${paramIndex++}`);
      values.push(reason);
    }
    if (requiresApproval !== undefined) {
      updateFields.push(`requires_approval = $${paramIndex++}`);
      values.push(requiresApproval);
    }
    if (status !== undefined) {
      updateFields.push(`status = $${paramIndex++}`);
      values.push(status);
      if (status === 'approved' && approvedBy) {
        updateFields.push(`approved_by = $${paramIndex++}`);
        values.push(approvedBy);
        updateFields.push(`approved_at = NOW()`);
        if (approvedSignature) {
          updateFields.push(`approved_signature = $${paramIndex++}`);
          values.push(approvedSignature);
        }
      } else if (status === 'rejected' && rejectedBy) {
        updateFields.push(`rejected_by = $${paramIndex++}`);
        values.push(rejectedBy);
        updateFields.push(`rejected_at = NOW()`);
        if (rejectionReason) {
          updateFields.push(`rejection_reason = $${paramIndex++}`);
          values.push(rejectionReason);
        }
      }
    }
    if (notes !== undefined) {
      updateFields.push(`notes = $${paramIndex++}`);
      values.push(notes);
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`);
    values.push(id, absenceId);

    const result = await query(
      `UPDATE employee_absences SET ${updateFields.join(', ')} 
       WHERE employee_id = $${paramIndex++} AND id = $${paramIndex} RETURNING 
       id, employee_id AS "employeeId", absence_type AS "absenceType", start_date AS "startDate", end_date AS "endDate",
       start_time AS "startTime", end_time AS "endTime", days_count AS "daysCount", reason, requires_approval AS "requiresApproval",
       status, approved_by AS "approvedBy", approved_at AS "approvedAt", approved_signature AS "approvedSignature",
       rejected_by AS "rejectedBy", rejected_at AS "rejectedAt", rejection_reason AS "rejectionReason",
       notes, created_at AS "createdAt", updated_at AS "updatedAt"`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Отсутствие не найдено' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating absence:', err);
    res.status(500).json({ error: 'Failed to update absence' });
  }
});

// DELETE /api/employees/:id/absences/:absenceId -> удаление отгула/прогула
app.delete(`${API_PREFIX}/employees/:id/absences/:absenceId`, async (req, res) => {
  try {
    const { id, absenceId } = req.params;
    const result = await query(
      'DELETE FROM employee_absences WHERE employee_id = $1 AND id = $2 RETURNING id',
      [id, absenceId]
    );
    if (result.length === 0) {
      return res.status(404).json({ error: 'Отсутствие не найдено' });
    }
    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting absence:', err);
    res.status(500).json({ error: 'Failed to delete absence' });
  }
});

// ========= УВОЛЬНЕНИЯ =========

// GET /api/employees/:id/terminations -> список увольнений сотрудника
app.get(`${API_PREFIX}/employees/:id/terminations`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      `SELECT id, employee_id AS "employeeId", termination_date AS "terminationDate", reason, initiated_by AS "initiatedBy", 
       initiated_at AS "initiatedAt", status, termination_contract_number AS "terminationContractNumber", 
       termination_contract_date AS "terminationContractDate", termination_contract_file_url AS "terminationContractFileUrl",
       final_settlement_amount AS "finalSettlementAmount", unused_vacation_days AS "unusedVacationDays", 
       compensation_amount AS "compensationAmount", severance_pay AS "severancePay", other_payments AS "otherPayments", 
       deductions, settlement_document_number AS "settlementDocumentNumber", settlement_document_date AS "settlementDocumentDate", 
       settlement_document_file_url AS "settlementDocumentFileUrl", notes, completed_at AS "completedAt", 
       created_at AS "createdAt", updated_at AS "updatedAt" 
       FROM employee_terminations WHERE employee_id = $1 ORDER BY termination_date DESC`,
      [id]
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching terminations:', err);
    res.status(500).json({ error: 'Failed to fetch terminations' });
  }
});

// POST /api/employees/:id/terminations -> создание увольнения (с автоматическим созданием договора и расчетов)
app.post(`${API_PREFIX}/employees/:id/terminations`, async (req, res) => {
  try {
    const { id } = req.params;
    const { 
      terminationDate, 
      reason, 
      initiatedBy,
      terminationContractNumber,
      terminationContractDate,
      terminationContractFileUrl,
      finalSettlementAmount,
      unusedVacationDays,
      compensationAmount,
      severancePay,
      otherPayments,
      deductions,
      settlementDocumentNumber,
      settlementDocumentDate,
      settlementDocumentFileUrl,
      notes
    } = req.body;

    if (!terminationDate || !reason || !initiatedBy) {
      return res.status(400).json({ error: 'terminationDate, reason и initiatedBy обязательны' });
    }

    // Проверяем существование сотрудника
    const employee = await query('SELECT id, name, salary FROM employees WHERE id = $1', [id]);
    if (employee.length === 0) {
      return res.status(404).json({ error: 'Сотрудник не найден' });
    }

    // Автоматически рассчитываем компенсацию за неиспользованный отпуск, если не указана
    let calculatedCompensation = compensationAmount;
    if (!calculatedCompensation && unusedVacationDays) {
      // Примерный расчет: средний дневной заработок * количество дней
      const dailySalary = employee[0].salary / 30; // Упрощенный расчет
      calculatedCompensation = dailySalary * unusedVacationDays;
    }

    // Автоматически генерируем номер договора, если не указан
    const contractNumber = terminationContractNumber || `УВ-${Date.now()}`;
    const contractDate = terminationContractDate || terminationDate;

    // Автоматически генерируем номер документа расчета, если не указан
    const docNumber = settlementDocumentNumber || `РАС-${Date.now()}`;
    const docDate = settlementDocumentDate || terminationDate;

    const result = await query(
      `INSERT INTO employee_terminations (
        employee_id, termination_date, reason, initiated_by,
        termination_contract_number, termination_contract_date, termination_contract_file_url,
        final_settlement_amount, unused_vacation_days, compensation_amount, severance_pay, other_payments, deductions,
        settlement_document_number, settlement_document_date, settlement_document_file_url,
        notes, status
      )
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, 'initiated') RETURNING 
       id, employee_id AS "employeeId", termination_date AS "terminationDate", reason, initiated_by AS "initiatedBy", 
       initiated_at AS "initiatedAt", status, termination_contract_number AS "terminationContractNumber", 
       termination_contract_date AS "terminationContractDate", termination_contract_file_url AS "terminationContractFileUrl",
       final_settlement_amount AS "finalSettlementAmount", unused_vacation_days AS "unusedVacationDays", 
       compensation_amount AS "compensationAmount", severance_pay AS "severancePay", other_payments AS "otherPayments", 
       deductions, settlement_document_number AS "settlementDocumentNumber", settlement_document_date AS "settlementDocumentDate", 
       settlement_document_file_url AS "settlementDocumentFileUrl", notes, completed_at AS "completedAt", 
       created_at AS "createdAt", updated_at AS "updatedAt"`,
      [
        id, terminationDate, reason, initiatedBy,
        contractNumber, contractDate, terminationContractFileUrl || null,
        finalSettlementAmount || null, unusedVacationDays || null, calculatedCompensation || null, 
        severancePay || null, otherPayments || null, deductions || null,
        docNumber, docDate, settlementDocumentFileUrl || null,
        notes || null
      ]
    );

    // Обновляем статус сотрудника на 'inactive'
    await query('UPDATE employees SET status = $1, updated_at = NOW() WHERE id = $2', ['inactive', id]);

    // Переносим задачи и ответственность на руководителя
    await transferResponsibilityToManager(id);

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating termination:', err);
    res.status(500).json({ error: 'Failed to create termination' });
  }
});

// PUT /api/employees/:id/terminations/:terminationId -> обновление увольнения
app.put(`${API_PREFIX}/employees/:id/terminations/:terminationId`, async (req, res) => {
  try {
    const { id, terminationId } = req.params;
    const updateData = req.body;

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    const allowedFields = [
      'terminationDate', 'reason', 'status',
      'terminationContractNumber', 'terminationContractDate', 'terminationContractFileUrl',
      'finalSettlementAmount', 'unusedVacationDays', 'compensationAmount', 
      'severancePay', 'otherPayments', 'deductions',
      'settlementDocumentNumber', 'settlementDocumentDate', 'settlementDocumentFileUrl',
      'notes'
    ];

    for (const field of allowedFields) {
      if (updateData[field] !== undefined) {
        const dbField = field.replace(/([A-Z])/g, '_$1').toLowerCase();
        updateFields.push(`${dbField} = $${paramIndex++}`);
        values.push(updateData[field]);
      }
    }

    if (updateData.status === 'completed') {
      updateFields.push(`completed_at = NOW()`);
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`);
    values.push(id, terminationId);

    const result = await query(
      `UPDATE employee_terminations SET ${updateFields.join(', ')} 
       WHERE employee_id = $${paramIndex++} AND id = $${paramIndex} RETURNING 
       id, employee_id AS "employeeId", termination_date AS "terminationDate", reason, initiated_by AS "initiatedBy", 
       initiated_at AS "initiatedAt", status, termination_contract_number AS "terminationContractNumber", 
       termination_contract_date AS "terminationContractDate", termination_contract_file_url AS "terminationContractFileUrl",
       final_settlement_amount AS "finalSettlementAmount", unused_vacation_days AS "unusedVacationDays", 
       compensation_amount AS "compensationAmount", severance_pay AS "severancePay", other_payments AS "otherPayments", 
       deductions, settlement_document_number AS "settlementDocumentNumber", settlement_document_date AS "settlementDocumentDate", 
       settlement_document_file_url AS "settlementDocumentFileUrl", notes, completed_at AS "completedAt", 
       created_at AS "createdAt", updated_at AS "updatedAt"`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Увольнение не найдено' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating termination:', err);
    res.status(500).json({ error: 'Failed to update termination' });
  }
});

// DELETE /api/employees/:id/terminations/:terminationId -> удаление увольнения
app.delete(`${API_PREFIX}/employees/:id/terminations/:terminationId`, async (req, res) => {
  try {
    const { id, terminationId } = req.params;
    const result = await query(
      'DELETE FROM employee_terminations WHERE employee_id = $1 AND id = $2 RETURNING id',
      [id, terminationId]
    );
    if (result.length === 0) {
      return res.status(404).json({ error: 'Увольнение не найдено' });
    }
    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting termination:', err);
    res.status(500).json({ error: 'Failed to delete termination' });
  }
});

// ========= ПАНЕЛЬ УПРАВЛЕНИЯ: ПОРТАЛЬНЫЕ ПОЛЬЗОВАТЕЛИ =========

// GET /api/portal-users/me?login=xxx -> текущий пользователь по логину (роль для ROLE_ACCESS)
app.get(`${API_PREFIX}/portal-users/me`, async (req, res) => {
  try {
    const login = (req.query.login || '').toString().trim();
    if (!login) {
      return res.status(400).json({ error: 'Укажите login' });
    }
    const rows = await query(`
      SELECT pu.id, pu.employee_id AS "employeeId", pu.login, pu.email, pu.role,
             e.name AS "employeeName", e.position AS "employeePosition"
      FROM portal_users pu
      JOIN employees e ON e.id = pu.employee_id
      WHERE pu.login = $1
    `, [login]);
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Пользователь не найден' });
    }
    res.json(rows[0]);
  } catch (err) {
    console.error('Error fetching portal user by login:', err);
    res.status(500).json({ error: 'Failed to fetch user' });
  }
});

// GET /api/admin/portal-users -> список пользователей портала (сотрудник, логин, роль, permissions, scope)
app.get(`${API_PREFIX}/admin/portal-users`, async (req, res) => {
  try {
    const rows = await query(`
      SELECT 
        pu.id,
        pu.employee_id AS "employeeId",
        pu.login,
        pu.email,
        pu.role,
        pu.permissions,
        pu.scope,
        pu.created_at AS "createdAt",
        e.name AS "employeeName",
        e.position AS "employeePosition"
      FROM portal_users pu
      JOIN employees e ON e.id = pu.employee_id
      ORDER BY e.name
    `);
    res.json(rows.map(r => ({ ...r, scope: r.scope || 'all' })));
  } catch (err) {
    console.error('Error fetching portal users:', err);
    res.status(500).json({ error: 'Failed to fetch portal users' });
  }
});

// POST /api/admin/portal-users -> создание пользователя (только привязанный к сотруднику)
app.post(`${API_PREFIX}/admin/portal-users`, async (req, res) => {
  try {
    const { employeeId, login, email, role, password } = req.body;
    if (!employeeId || !login || !login.trim()) {
      return res.status(400).json({ error: 'employeeId и login обязательны', code: 'VALIDATION' });
    }
    const employeeCheck = await query('SELECT id FROM employees WHERE id = $1', [employeeId]);
    if (employeeCheck.length === 0) {
      return res.status(400).json({ error: 'Сотрудник не найден. Создайте сначала сотрудника в разделе Кадры.', code: 'NO_EMPLOYEE' });
    }
    const existingLogin = await query('SELECT id FROM portal_users WHERE login = $1', [login.trim()]);
    if (existingLogin.length > 0) {
      return res.status(400).json({ error: 'Пользователь с таким логином уже существует', code: 'LOGIN_EXISTS' });
    }
    const existingForEmployee = await query('SELECT id FROM portal_users WHERE employee_id = $1', [employeeId]);
    if (existingForEmployee.length > 0) {
      return res.status(400).json({ error: 'У этого сотрудника уже есть пользователь портала', code: 'EMPLOYEE_HAS_USER' });
    }
    const roleValue = (role && ['DIRECTOR', 'ENGINEER', 'MASTER', 'LAWYER', 'FINANCIER', 'HR_MANAGER', 'PR_MANAGER'].includes(role)) ? role : 'ENGINEER';
    let passwordHash = null;
    if (password && String(password).trim()) {
      passwordHash = await bcrypt.hash(String(password).trim(), 10);
    }
    await query(
      'INSERT INTO portal_users (employee_id, login, email, role, password_hash) VALUES ($1, $2, $3, $4, $5)',
      [employeeId, login.trim(), email && email.trim() ? email.trim() : null, roleValue, passwordHash]
    );
    const created = await query(`
      SELECT pu.id, pu.employee_id AS "employeeId", pu.login, pu.email, pu.role,
             e.name AS "employeeName", e.position AS "employeePosition"
      FROM portal_users pu JOIN employees e ON e.id = pu.employee_id
      WHERE pu.employee_id = $1
    `, [employeeId]);
    res.status(201).json(created[0] || { employeeId, login: login.trim(), email: email || null, role: roleValue });
  } catch (err) {
    console.error('Error creating portal user:', err && err.message ? err.message : err);
    res.status(500).json({ error: 'Failed to create portal user' });
  }
});

// POST /api/admin/employees-with-user -> создать сотрудника и сразу пользователя портала
app.post(`${API_PREFIX}/admin/employees-with-user`, async (req, res) => {
  try {
    const { name, position, phone, status, salary, assignedDistrictId, login, email, role } = req.body;
    if (!name || !position || !phone || !salary) {
      return res.status(400).json({ error: 'name, position, phone и salary обязательны' });
    }
    if (!login || !login.trim()) {
      return res.status(400).json({ error: 'login обязателен для пользователя портала' });
    }
    const existingLogin = await query('SELECT id FROM portal_users WHERE login = $1', [login.trim()]);
    if (existingLogin.length > 0) {
      return res.status(400).json({ error: 'Пользователь с таким логином уже существует' });
    }
    const employeeId = `e-${Date.now()}`;
    const employeeStatus = status || 'active';
    await query(
      `INSERT INTO employees (id, name, position, phone, status, salary, assigned_district_id)
       VALUES ($1, $2, $3, $4, $5, $6, $7)`,
      [employeeId, name.trim(), position.trim(), phone.trim(), employeeStatus, Number(salary) || 0, assignedDistrictId && assignedDistrictId.trim() ? assignedDistrictId.trim() : null]
    );
    const roleValue = (role && ['DIRECTOR', 'ENGINEER', 'MASTER', 'LAWYER', 'FINANCIER', 'HR_MANAGER', 'PR_MANAGER'].includes(role)) ? role : 'ENGINEER';
    await query(
      'INSERT INTO portal_users (employee_id, login, email, role) VALUES ($1, $2, $3, $4)',
      [employeeId, login.trim(), email && email.trim() ? email.trim() : null, roleValue]
    );
    const portalUser = await query(`
      SELECT pu.id, pu.employee_id AS "employeeId", pu.login, pu.email, pu.role,
             e.name AS "employeeName", e.position AS "employeePosition"
      FROM portal_users pu JOIN employees e ON e.id = pu.employee_id
      WHERE pu.employee_id = $1
    `, [employeeId]);
    res.status(201).json({
      employee: { id: employeeId, name: name.trim(), position: position.trim(), phone: phone.trim(), status: employeeStatus, salary: Number(salary) || 0, assignedDistrictId: assignedDistrictId || null },
      portalUser: portalUser[0],
    });
  } catch (err) {
    console.error('Error creating employee with user:', err && err.message ? err.message : err);
    res.status(500).json({ error: 'Failed to create employee and user' });
  }
});

// PUT /api/admin/portal-users/:id -> обновление пользователя (логин, email, роль, permissions, scope)
app.put(`${API_PREFIX}/admin/portal-users/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { login, email, role, permissions, scope } = req.body;
    const existing = await query('SELECT id FROM portal_users WHERE id = $1', [id]);
    if (existing.length === 0) {
      return res.status(404).json({ error: 'Пользователь не найден' });
    }
    const updates = [];
    const values = [];
    let idx = 1;
    if (login !== undefined) {
      const loginTrim = login && String(login).trim();
      if (!loginTrim) {
        return res.status(400).json({ error: 'Логин не может быть пустым' });
      }
      const dup = await query('SELECT id FROM portal_users WHERE login = $1 AND id != $2', [loginTrim, id]);
      if (dup.length > 0) {
        return res.status(400).json({ error: 'Пользователь с таким логином уже существует' });
      }
      updates.push(`login = $${idx++}`);
      values.push(loginTrim);
    }
    if (email !== undefined) {
      updates.push(`email = $${idx++}`);
      values.push(email && String(email).trim() ? String(email).trim() : null);
    }
    if (role !== undefined) {
      const roleValue = ['DIRECTOR', 'ENGINEER', 'MASTER', 'LAWYER', 'FINANCIER', 'HR_MANAGER', 'PR_MANAGER'].includes(role) ? role : 'ENGINEER';
      updates.push(`role = $${idx++}`);
      values.push(roleValue);
    }
    if (permissions !== undefined) {
      const perms = Array.isArray(permissions) ? permissions : null;
      updates.push(`permissions = $${idx++}`);
      values.push(perms ? JSON.stringify(perms) : null);
    }
    if (scope !== undefined) {
      const scopeVal = scope === 'own_district' ? 'own_district' : 'all';
      updates.push(`scope = $${idx++}`);
      values.push(scopeVal);
    }
    if (updates.length === 0) {
      const row = await query(`
        SELECT pu.id, pu.employee_id AS "employeeId", pu.login, pu.email, pu.role, pu.permissions, pu.scope,
               e.name AS "employeeName", e.position AS "employeePosition"
        FROM portal_users pu JOIN employees e ON e.id = pu.employee_id WHERE pu.id = $1
      `, [id]);
      return res.json({ ...row[0], scope: row[0].scope || 'all' });
    }
    updates.push('updated_at = NOW()');
    values.push(id);
    await query(
      `UPDATE portal_users SET ${updates.join(', ')} WHERE id = $${idx}`,
      values
    );
    const row = await query(`
      SELECT pu.id, pu.employee_id AS "employeeId", pu.login, pu.email, pu.role, pu.permissions, pu.scope,
             e.name AS "employeeName", e.position AS "employeePosition"
      FROM portal_users pu JOIN employees e ON e.id = pu.employee_id WHERE pu.id = $1
    `, [id]);
    res.json({ ...row[0], scope: row[0].scope || 'all' });
  } catch (err) {
    console.error('Error updating portal user:', err && err.message ? err.message : err);
    res.status(500).json({ error: 'Failed to update portal user' });
  }
});

// DELETE /api/admin/portal-users/:id -> удаление пользователя портала
app.delete(`${API_PREFIX}/admin/portal-users/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM portal_users WHERE id = $1 RETURNING id', [id]);
    if (result.length === 0) {
      return res.status(404).json({ error: 'Пользователь не найден' });
    }
    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting portal user:', err);
    res.status(500).json({ error: 'Failed to delete portal user' });
  }
});

// ========= ПАНЕЛЬ УПРАВЛЕНИЯ: БЕЗОПАСНОСТЬ (дашборд) =========

// GET /api/admin/security/settings — настройки капчи (из БД или env)
app.get(`${API_PREFIX}/admin/security/settings`, async (req, res) => {
  try {
    const rows = await query('SELECT site_key, secret_key FROM security_captcha WHERE id = 1');
    const siteKey = (rows[0] && rows[0].site_key && String(rows[0].site_key).trim()) || null;
    const hasSecret = !!(rows[0] && rows[0].secret_key && String(rows[0].secret_key).trim());
    const captchaEnabled = hasSecret || !!process.env.TURNSTILE_SECRET_KEY;
    res.json({ captchaEnabled, turnstileSiteKey: siteKey, turnstileSecretKeySet: hasSecret });
  } catch (err) {
    res.status(500).json({ error: 'Ошибка загрузки настроек' });
  }
});

// PUT /api/admin/security/settings — сохранить ключи Turnstile (капча)
app.put(`${API_PREFIX}/admin/security/settings`, async (req, res) => {
  try {
    const { turnstileSiteKey, turnstileSecretKey } = req.body || {};
    const siteKey = turnstileSiteKey != null ? String(turnstileSiteKey).trim() || null : undefined;
    const secretKey = turnstileSecretKey !== undefined ? (String(turnstileSecretKey).trim() || null) : undefined;
    const existing = await query('SELECT site_key, secret_key FROM security_captcha WHERE id = 1');
    if (existing.length === 0) {
      await query(
        'INSERT INTO security_captcha (id, site_key, secret_key, updated_at) VALUES (1, $1, $2, NOW())',
        [siteKey !== undefined ? siteKey : null, secretKey !== undefined ? secretKey : null]
      );
    } else {
      const newSiteKey = siteKey !== undefined ? siteKey : (existing[0].site_key && String(existing[0].site_key).trim()) || null;
      const newSecretKey = secretKey !== undefined ? secretKey : (existing[0].secret_key && String(existing[0].secret_key).trim()) || null;
      await query(
        'UPDATE security_captcha SET site_key = $1, secret_key = $2, updated_at = NOW() WHERE id = 1',
        [newSiteKey, newSecretKey]
      );
    }
    const rows = await query('SELECT site_key, secret_key FROM security_captcha WHERE id = 1');
    const r = rows[0];
    const hasSecret = !!(r && r.secret_key && String(r.secret_key).trim());
    res.json({
      captchaEnabled: hasSecret || !!process.env.TURNSTILE_SECRET_KEY,
      turnstileSiteKey: (r && r.site_key && String(r.site_key).trim()) || null,
      turnstileSecretKeySet: hasSecret
    });
  } catch (err) {
    console.error('Error saving security settings:', err.message || err);
    res.status(500).json({ error: 'Ошибка сохранения настроек' });
  }
});

// GET /api/admin/security/logs — логи входа (пагинация, фильтр по успеху)
app.get(`${API_PREFIX}/admin/security/logs`, async (req, res) => {
  try {
    const page = Math.max(1, parseInt(req.query.page, 10) || 1);
    const limit = Math.min(100, Math.max(1, parseInt(req.query.limit, 10) || 50));
    const offset = (page - 1) * limit;
    const successFilter = req.query.success;
    const from = req.query.from;
    const to = req.query.to;
    let where = [];
    const params = [];
    let idx = 1;
    if (successFilter === 'true' || successFilter === 'false') {
      where.push(`success = $${idx++}`);
      params.push(successFilter === 'true');
    }
    if (from) {
      where.push(`created_at >= $${idx++}`);
      params.push(from);
    }
    if (to) {
      where.push(`created_at <= $${idx++}`);
      params.push(to);
    }
    const whereClause = where.length ? ' WHERE ' + where.join(' AND ') : '';
    const countResult = await query(
      `SELECT COUNT(*) AS total FROM auth_logs${whereClause}`,
      params
    );
    const total = parseInt(countResult[0]?.total || 0, 10);
    params.push(limit, offset);
    const rows = await query(
      `SELECT id, ip, login_masked AS "loginMasked", success, created_at AS "createdAt"
       FROM auth_logs${whereClause}
       ORDER BY created_at DESC LIMIT $${idx++} OFFSET $${idx}`,
      params
    );
    res.json({ items: rows, total, page, limit });
  } catch (err) {
    console.error('Error fetching auth logs:', err.message || err);
    res.status(500).json({ error: 'Ошибка загрузки логов' });
  }
});

// GET /api/admin/security/stats — сводка для мониторинга
app.get(`${API_PREFIX}/admin/security/stats`, async (req, res) => {
  try {
    const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
    const failed = await query(
      'SELECT COUNT(*) AS c FROM auth_logs WHERE success = false AND created_at >= $1',
      [since]
    );
    const total = await query(
      'SELECT COUNT(*) AS c FROM auth_logs WHERE created_at >= $1',
      [since]
    );
    const blacklistCount = await query('SELECT COUNT(*) AS c FROM security_blacklist');
    res.json({
      failedLast24h: parseInt(failed[0]?.c || 0, 10),
      totalAttemptsLast24h: parseInt(total[0]?.c || 0, 10),
      blockedCount: parseInt(blacklistCount[0]?.c || 0, 10)
    });
  } catch (err) {
    res.json({ failedLast24h: 0, totalAttemptsLast24h: 0, blockedCount: 0 });
  }
});

// GET /api/admin/security/blacklist — чёрный список
app.get(`${API_PREFIX}/admin/security/blacklist`, async (req, res) => {
  try {
    const rows = await query(
      `SELECT id, type, value, reason, created_at AS "createdAt"
       FROM security_blacklist ORDER BY created_at DESC`
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching blacklist:', err.message || err);
    res.status(500).json({ error: 'Ошибка загрузки чёрного списка' });
  }
});

// POST /api/admin/security/blacklist — добавить в чёрный список
app.post(`${API_PREFIX}/admin/security/blacklist`, async (req, res) => {
  try {
    const { type, value, reason } = req.body;
    if (!type || !value || !['ip', 'login'].includes(type)) {
      return res.status(400).json({ error: 'Укажите type (ip или login) и value' });
    }
    const val = String(value).trim();
    if (!val) return res.status(400).json({ error: 'value не может быть пустым' });
    const rows = await query(
      `INSERT INTO security_blacklist (type, value, reason) VALUES ($1, $2, $3)
       RETURNING id, type, value, reason, created_at AS "createdAt"`,
      [type, val, reason && String(reason).trim() ? String(reason).trim() : null]
    );
    res.status(201).json(rows[0]);
  } catch (err) {
    if (err.code === '23505') return res.status(400).json({ error: 'Такая запись уже есть в чёрном списке' });
    console.error('Error adding to blacklist:', err.message || err);
    res.status(500).json({ error: 'Ошибка добавления' });
  }
});

// DELETE /api/admin/security/blacklist/:id — удалить из чёрного списка
app.delete(`${API_PREFIX}/admin/security/blacklist/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM security_blacklist WHERE id = $1 RETURNING id', [id]);
    if (result.length === 0) return res.status(404).json({ error: 'Запись не найдена' });
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Ошибка удаления' });
  }
});

// ========= ПАНЕЛЬ УПРАВЛЕНИЯ: ШАБЛОНЫ ПРАВ =========
const PERMISSION_SECTION_KEYS = ['dashboard', 'objects', 'requests', 'pr', 'finance', 'legal', 'development', 'hr', 'office', 'admin'];

app.get(`${API_PREFIX}/admin/permission-templates`, async (req, res) => {
  try {
    const rows = await query(`
      SELECT id, name, description, permissions, scope, for_position AS "forPosition", suggested_role AS "suggestedRole",
             created_at AS "createdAt", updated_at AS "updatedAt"
      FROM permission_templates ORDER BY name
    `);
    res.json(rows.map(r => ({
      ...r,
      permissions: r.permissions || [],
      scope: r.scope || 'all',
      forPosition: r.forPosition || null,
      suggestedRole: r.suggestedRole || null
    })));
  } catch (err) {
    console.error('Error fetching permission templates:', err);
    res.status(500).json({ error: 'Failed to fetch permission templates' });
  }
});

app.post(`${API_PREFIX}/admin/permission-templates`, async (req, res) => {
  try {
    const { name, description, permissions, scope, forPosition, suggestedRole } = req.body;
    if (!name || !String(name).trim()) {
      return res.status(400).json({ error: 'Укажите название шаблона' });
    }
    const perms = Array.isArray(permissions) ? permissions : [];
    const scopeVal = scope === 'own_district' ? 'own_district' : 'all';
    const rows = await query(
      `INSERT INTO permission_templates (name, description, permissions, scope, for_position, suggested_role)
       VALUES ($1, $2, $3, $4, $5, $6)
       RETURNING id, name, description, permissions, scope, for_position AS "forPosition", suggested_role AS "suggestedRole", created_at AS "createdAt", updated_at AS "updatedAt"`,
      [
        String(name).trim(),
        description && String(description).trim() ? String(description).trim() : null,
        JSON.stringify(perms),
        scopeVal,
        forPosition && String(forPosition).trim() ? String(forPosition).trim() : null,
        suggestedRole && ['DIRECTOR', 'ENGINEER', 'MASTER', 'LAWYER', 'FINANCIER', 'HR_MANAGER', 'PR_MANAGER'].includes(suggestedRole) ? suggestedRole : null
      ]
    );
    const r = rows[0];
    res.status(201).json({ ...r, permissions: r.permissions || [], scope: r.scope || 'all', forPosition: r.forPosition || null, suggestedRole: r.suggestedRole || null });
  } catch (err) {
    console.error('Error creating permission template:', err);
    res.status(500).json({ error: 'Failed to create permission template' });
  }
});

app.put(`${API_PREFIX}/admin/permission-templates/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { name, description, permissions, scope, forPosition, suggestedRole } = req.body;
    const existing = await query('SELECT id FROM permission_templates WHERE id = $1', [id]);
    if (existing.length === 0) {
      return res.status(404).json({ error: 'Шаблон не найден' });
    }
    const updates = [];
    const values = [];
    let idx = 1;
    if (name !== undefined) {
      updates.push(`name = $${idx++}`);
      values.push(String(name).trim());
    }
    if (description !== undefined) {
      updates.push(`description = $${idx++}`);
      values.push(description && String(description).trim() ? String(description).trim() : null);
    }
    if (permissions !== undefined) {
      updates.push(`permissions = $${idx++}`);
      values.push(JSON.stringify(Array.isArray(permissions) ? permissions : []));
    }
    if (scope !== undefined) {
      updates.push(`scope = $${idx++}`);
      values.push(scope === 'own_district' ? 'own_district' : 'all');
    }
    if (forPosition !== undefined) {
      updates.push(`for_position = $${idx++}`);
      values.push(forPosition && String(forPosition).trim() ? String(forPosition).trim() : null);
    }
    if (suggestedRole !== undefined) {
      updates.push(`suggested_role = $${idx++}`);
      values.push(suggestedRole && ['DIRECTOR', 'ENGINEER', 'MASTER', 'LAWYER', 'FINANCIER', 'HR_MANAGER', 'PR_MANAGER'].includes(suggestedRole) ? suggestedRole : null);
    }
    if (updates.length === 0) {
      const row = await query('SELECT id, name, description, permissions, scope, for_position AS "forPosition", suggested_role AS "suggestedRole", created_at AS "createdAt", updated_at AS "updatedAt" FROM permission_templates WHERE id = $1', [id]);
      const r = row[0];
      return res.json({ ...r, permissions: r.permissions || [], scope: r.scope || 'all', forPosition: r.forPosition || null, suggestedRole: r.suggestedRole || null });
    }
    updates.push('updated_at = NOW()');
    values.push(id);
    await query(`UPDATE permission_templates SET ${updates.join(', ')} WHERE id = $${idx}`, values);
    const row = await query('SELECT id, name, description, permissions, scope, for_position AS "forPosition", suggested_role AS "suggestedRole", created_at AS "createdAt", updated_at AS "updatedAt" FROM permission_templates WHERE id = $1', [id]);
    const r = row[0];
    res.json({ ...r, permissions: r.permissions || [], scope: r.scope || 'all', forPosition: r.forPosition || null, suggestedRole: r.suggestedRole || null });
  } catch (err) {
    console.error('Error updating permission template:', err);
    res.status(500).json({ error: 'Failed to update permission template' });
  }
});

app.delete(`${API_PREFIX}/admin/permission-templates/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM permission_templates WHERE id = $1 RETURNING id', [id]);
    if (result.length === 0) {
      return res.status(404).json({ error: 'Шаблон не найден' });
    }
    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting permission template:', err);
    res.status(500).json({ error: 'Failed to delete permission template' });
  }
});

// ========= ПАНЕЛЬ УПРАВЛЕНИЯ: НАСТРОЙКИ ИНТЕГРАЦИЙ =========

/** Получить настройки Дома.АИ из БД (integration_settings key=doma), при отсутствии — из env */
async function getDomaSettingsFromDb() {
  try {
    const rows = await query(
      'SELECT enabled, config FROM integration_settings WHERE key = $1',
      ['doma']
    );
    if (rows.length > 0 && rows[0].config) {
      const c = rows[0].config;
      const apiUrl = (c.apiUrl && String(c.apiUrl).trim()) || process.env.DOMA_API_URL || '';
      const token = (c.token && String(c.token).trim()) || process.env.DOMA_API_TOKEN || '';
      if (apiUrl) return { apiUrl, token };
    }
  } catch (err) {
    console.warn('[backend] getDomaSettingsFromDb:', err.message);
  }
  return {
    apiUrl: process.env.DOMA_API_URL || '',
    token: process.env.DOMA_API_TOKEN || '',
  };
}

// GET /api/admin/integrations/doma -> настройки Дома.АИ (apiUrl, token из config)
app.get(`${API_PREFIX}/admin/integrations/doma`, async (req, res) => {
  try {
    const rows = await query(
      'SELECT enabled, config FROM integration_settings WHERE key = $1',
      ['doma']
    );
    if (rows.length === 0) {
      return res.json({ apiUrl: '', token: '' });
    }
    const r = rows[0];
    const config = r.config || {};
    res.json({
      apiUrl: config.apiUrl || '',
      token: config.token || '',
    });
  } catch (err) {
    console.error('Error fetching doma settings:', err);
    res.status(500).json({ error: 'Failed to fetch doma settings' });
  }
});

// PUT /api/admin/integrations/doma -> сохранить настройки Дома.АИ
app.put(`${API_PREFIX}/admin/integrations/doma`, async (req, res) => {
  try {
    const { apiUrl, token } = req.body;
    const config = {
      apiUrl: apiUrl != null ? String(apiUrl).trim() : '',
      token: token != null ? String(token).trim() : '',
    };
    await query(
      `INSERT INTO integration_settings (key, name, enabled, config, updated_at)
       VALUES ('doma', 'Дома.АИ (заявки, API)', true, $1, NOW())
       ON CONFLICT (key) DO UPDATE SET config = $1, updated_at = NOW()`,
      [JSON.stringify(config)]
    );
    res.json({ success: true });
    // Автоматически запускаем синхронизацию заявок после сохранения настроек (без ожидания ответа)
    if (config.apiUrl && config.token) {
      syncDomaApplications().catch((err) => {
        console.error('[backend] Ошибка автосинхронизации после сохранения настроек Doma AI:', err.message || err);
      });
    }
  } catch (err) {
    console.error('Error saving doma settings:', err);
    res.status(500).json({ error: 'Failed to save doma settings' });
  }
});

// GET /api/admin/integrations/dadata -> настройки DaData (enabled, apiKey, secret из config)
app.get(`${API_PREFIX}/admin/integrations/dadata`, async (req, res) => {
  try {
    const rows = await query(
      'SELECT enabled, config FROM integration_settings WHERE key = $1',
      ['dadata']
    );
    if (rows.length === 0) {
      return res.json({ enabled: true, apiKey: '', secret: '' });
    }
    const r = rows[0];
    const config = r.config || {};
    res.json({
      enabled: r.enabled !== false,
      apiKey: config.apiKey || '',
      secret: config.secret || '',
    });
  } catch (err) {
    console.error('Error fetching dadata settings:', err);
    res.status(500).json({ error: 'Failed to fetch dadata settings' });
  }
});

// PUT /api/admin/integrations/dadata -> сохранить настройки DaData
app.put(`${API_PREFIX}/admin/integrations/dadata`, async (req, res) => {
  try {
    const { enabled, apiKey, secret } = req.body;
    const config = {
      apiKey: apiKey != null ? String(apiKey) : '',
      secret: secret != null ? String(secret) : '',
    };
    await query(
      `INSERT INTO integration_settings (key, name, enabled, config, updated_at)
       VALUES ('dadata', 'DaData (проверка контрагентов по ИНН)', $1, $2, NOW())
       ON CONFLICT (key) DO UPDATE SET enabled = $1, config = $2, updated_at = NOW()`,
      [enabled !== false, JSON.stringify(config)]
    );
    res.json({ success: true });
  } catch (err) {
    console.error('Error saving dadata settings:', err);
    res.status(500).json({ error: 'Failed to save dadata settings' });
  }
});

// GET /api/admin/integrations/ai-chat -> настройки ИИ-чата (enabled, url, apiKey из config)
app.get(`${API_PREFIX}/admin/integrations/ai-chat`, async (req, res) => {
  try {
    const rows = await query(
      'SELECT enabled, config FROM integration_settings WHERE key = $1',
      ['ai_chat']
    );
    if (rows.length === 0) {
      return res.json({ enabled: false, url: '', apiKey: '' });
    }
    const r = rows[0];
    const config = r.config || {};
    res.json({
      enabled: r.enabled !== false,
      url: config.url || '',
      apiKey: config.apiKey || '',
    });
  } catch (err) {
    console.error('Error fetching ai-chat settings:', err);
    res.status(500).json({ error: 'Failed to fetch ai-chat settings' });
  }
});

// PUT /api/admin/integrations/ai-chat -> сохранить настройки ИИ-чата
app.put(`${API_PREFIX}/admin/integrations/ai-chat`, async (req, res) => {
  try {
    const { enabled, url, apiKey } = req.body;
    const config = {
      url: url != null ? String(url).trim() : '',
      apiKey: apiKey != null ? String(apiKey) : '',
    };
    await query(
      `INSERT INTO integration_settings (key, name, enabled, config, updated_at)
       VALUES ('ai_chat', 'ИИ-помощник (чат)', $1, $2, NOW())
       ON CONFLICT (key) DO UPDATE SET enabled = $1, config = $2, updated_at = NOW()`,
      [enabled !== false, JSON.stringify(config)]
    );
    res.json({ success: true });
  } catch (err) {
    console.error('Error saving ai-chat settings:', err);
    res.status(500).json({ error: 'Failed to save ai-chat settings' });
  }
});

// ========= ПАНЕЛЬ УПРАВЛЕНИЯ: РЕЗЕРВНЫЕ КОПИИ БД =========

const backupDir = path.join(__dirname, 'backups');
if (!fs.existsSync(backupDir)) {
  fs.mkdirSync(backupDir, { recursive: true });
}

function safeBackupFilename(name) {
  return /^backup-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.sql$/.test(name) ? name : null;
}

// POST /api/admin/backup -> создать дамп БД (pg_dump)
app.post(`${API_PREFIX}/admin/backup`, async (req, res) => {
  try {
    if (!DATABASE_URL) {
      return res.status(503).json({ error: 'DATABASE_URL не настроен' });
    }
    const now = new Date();
    const filename = `backup-${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}-${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}-${String(now.getSeconds()).padStart(2, '0')}.sql`;
    const filepath = path.join(backupDir, filename);
    const url = new URL(DATABASE_URL);
    const dbName = url.pathname.slice(1) || 'mkd_control_center';
    const pgDump = spawn('pg_dump', [
      '-h', url.hostname || 'localhost',
      '-p', url.port || '5432',
      '-U', url.username || 'postgres',
      '-d', dbName,
      '-F', 'p',
      '-f', filepath
    ], {
      env: { ...process.env, PGPASSWORD: url.password || '' }
    });
    await new Promise((resolve, reject) => {
      pgDump.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`pg_dump exit ${code}`))));
      pgDump.on('error', reject);
    });
    res.json({ filename, createdAt: now.toISOString() });
  } catch (err) {
    console.error('Error creating backup:', err);
    res.status(500).json({
      error: 'Не удалось создать резервную копию. Убедитесь, что pg_dump доступен (PostgreSQL bin в PATH).',
      details: err.message
    });
  }
});

// GET /api/admin/backups -> список резервных копий
app.get(`${API_PREFIX}/admin/backups`, async (req, res) => {
  try {
    const files = fs.readdirSync(backupDir)
      .filter((f) => f.endsWith('.sql'))
      .map((f) => {
        const stat = fs.statSync(path.join(backupDir, f));
        return { filename: f, createdAt: stat.mtime.toISOString() };
      })
      .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
    res.json(files);
  } catch (err) {
    console.error('Error listing backups:', err);
    res.status(500).json({ error: 'Failed to list backups' });
  }
});

// GET /api/admin/backup/download/:filename -> скачать файл резервной копии
app.get(`${API_PREFIX}/admin/backup/download/:filename`, (req, res) => {
  const filename = safeBackupFilename(req.params.filename);
  if (!filename) {
    return res.status(400).json({ error: 'Недопустимое имя файла' });
  }
  const filepath = path.join(backupDir, filename);
  if (!fs.existsSync(filepath)) {
    return res.status(404).json({ error: 'Файл не найден' });
  }
  res.download(filepath, filename);
});

// ========= ПАНЕЛЬ УПРАВЛЕНИЯ: ЗАГРУЗКА ДАННЫХ =========

const templatesDir = path.join(__dirname, 'templates');
const ALLOWED_IMPORT_TYPES = ['districts', 'buildings', 'employees', 'accounts'];

// GET /api/admin/import/templates/:type -> скачать шаблон CSV
app.get(`${API_PREFIX}/admin/import/templates/:type`, (req, res) => {
  const type = req.params.type;
  if (!ALLOWED_IMPORT_TYPES.includes(type)) {
    return res.status(400).json({ error: 'Недопустимый тип: ' + type });
  }
  const filepath = path.join(templatesDir, `${type}.csv`);
  if (!fs.existsSync(filepath)) {
    return res.status(404).json({ error: 'Шаблон не найден' });
  }
  res.download(filepath, `${type}_template.csv`);
});

// POST /api/admin/import/:type -> загрузка файла (CSV/XLSX) и импорт
app.post(`${API_PREFIX}/admin/import/:type`, upload.single('file'), async (req, res) => {
  const type = req.params.type;
  if (!ALLOWED_IMPORT_TYPES.includes(type)) {
    return res.status(400).json({ error: 'Недопустимый тип: ' + type });
  }
  if (!req.file || !req.file.path) {
    return res.status(400).json({ error: 'Файл не загружен' });
  }
  const filePath = req.file.path;
  const ext = path.extname(req.file.originalname).toLowerCase();
  const fileType = ext === '.csv' ? 'CSV' : ext === '.xlsx' || ext === '.xls' ? 'XLSX' : null;
  if (!fileType) {
    try { fs.unlinkSync(filePath); } catch (e) {}
    return res.status(400).json({ error: 'Поддерживаются только CSV и XLSX' });
  }
  try {
    let rows = [];
    if (fileType === 'CSV') {
      rows = await fileProcessor.parseCSV(filePath, 'import');
    } else {
      rows = await fileProcessor.parseXLSX(filePath, 'import');
    }
    const errors = [];
    let created = 0;
    for (let i = 0; i < rows.length; i++) {
      const row = rows[i];
      const rowNum = i + 2;
      try {
        if (type === 'districts') {
          const id = (row.id || row.ID || '').trim();
          const name = (row.name || row.name_ru || '').trim();
          const managerName = (row.manager_name || row.managerName || '').trim();
          if (!id || !name || !managerName) {
            errors.push({ row: rowNum, message: 'Нужны id, name, manager_name', data: row });
            continue;
          }
          await query(
            'INSERT INTO districts (id, name, manager_name) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET name = $2, manager_name = $3',
            [id, name, managerName]
          );
          created++;
        } else if (type === 'employees') {
          const id = (row.id || row.ID || `e-${Date.now()}-${i}`).trim();
          const name = (row.name || row.name_ru || '').trim();
          const position = (row.position || '').trim() || 'Сотрудник';
          const phone = (row.phone || '').trim() || '+7';
          const status = (row.status || 'active').trim();
          const salary = parseFloat(row.salary) || 0;
          const assignedDistrictId = (row.assigned_district_id || row.assignedDistrictId || '').trim() || null;
          if (!name) {
            errors.push({ row: rowNum, message: 'Нужно имя (name)', data: row });
            continue;
          }
          await query(
            `INSERT INTO employees (id, name, position, phone, status, salary, assigned_district_id)
             VALUES ($1, $2, $3, $4, $5, $6, $7)
             ON CONFLICT (id) DO UPDATE SET name = $2, position = $3, phone = $4, status = $5, salary = $6, assigned_district_id = $7, updated_at = NOW()`,
            [id, name, position, phone, status, salary, assignedDistrictId]
          );
          created++;
        } else if (type === 'buildings') {
          const id = (row.id || row.ID || `b-${Date.now()}-${i}`).trim();
          const address = (row.address || '').trim();
          const districtId = (row.district_id || row.districtId || '').trim() || null;
          if (!id || !address) {
            errors.push({ row: rowNum, message: 'Нужны id и address', data: row });
            continue;
          }
          const buildingData = {
            id,
            passport: { address },
            districtId: districtId || undefined,
            accounts: [],
          };
          await query(
            'INSERT INTO buildings (id, data) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET data = $2',
            [id, JSON.stringify(buildingData)]
          );
          created++;
        } else if (type === 'accounts') {
          const buildingId = (row.building_id || row.buildingId || '').trim();
          const apartmentNumber = (row.apartment_number || row.apartmentNumber || row.apartment || '').trim();
          if (!buildingId || !apartmentNumber) {
            errors.push({ row: rowNum, message: 'Нужны building_id и apartment_number', data: row });
            continue;
          }
          const buildingRows = await query('SELECT id FROM buildings WHERE id = $1', [buildingId]);
          if (buildingRows.length === 0) {
            errors.push({ row: rowNum, message: 'Дом не найден: ' + buildingId, data: row });
            continue;
          }
          const existing = await query(
            `SELECT id FROM building_personal_accounts WHERE building_id = $1 AND data->>'apartmentNumber' = $2`,
            [buildingId, apartmentNumber]
          );
          if (existing.length === 0) {
            const accountId = `acc-${Date.now()}-${i}`;
            const accountData = {
              id: accountId,
              apartmentNumber,
              accountNumber: `${buildingId.replace('b-', '')}00${rowNum}`,
              type: 'apartment',
              floor: 0,
              owners: [],
              registered: [],
              areaTotal: 0,
              areaLiving: 0,
              areaNonLiving: 0,
              meters: [],
              isMeterInstallationFeasible: false
            };
            await query(
              'INSERT INTO building_personal_accounts (id, building_id, data) VALUES ($1, $2, $3)',
              [accountId, buildingId, accountData]
            );
            created++;
          }
        }
      } catch (err) {
        errors.push({ row: rowNum, message: err.message || 'Ошибка вставки', data: row });
      }
    }
    try { fs.unlinkSync(filePath); } catch (e) {}
    res.json({ created, errors, total: rows.length });
  } catch (err) {
    console.error('Error importing:', err);
    try { fs.unlinkSync(filePath); } catch (e) {}
    res.status(500).json({ error: 'Ошибка импорта: ' + (err.message || 'неизвестная ошибка') });
  }
});

// GET /api/applications -> список заявок
app.get(`${API_PREFIX}/applications`, async (req, res) => {
  try {
    // Проверяем наличие колонки is_overdue
    const hasIsOverdue = await query(`
      SELECT column_name 
      FROM information_schema.columns 
      WHERE table_name = 'applications' AND column_name = 'is_overdue'
    `);
    
    const includeIsOverdue = hasIsOverdue.length > 0;
    
    const rows = await query(
      `SELECT
         id,
         number,
         status,
         description,
         address,
         apartment,
         client_name AS "clientName",
         created_at AS "createdAt",
         deadline_at AS "deadlineAt",
         performer_name AS "performerName"
         ${includeIsOverdue ? ', is_overdue AS "isOverdue"' : ''}
         , doma_id AS "domaId"
       FROM applications
       ORDER BY created_at DESC`
    );

    const apps = rows.map((r) => ({
      id: r.id,
      number: r.number,
      status: r.status,
      description: r.description,
      address: r.address,
      apartment: r.apartment,
      clientName: r.clientName,
      createdAt: r.createdAt,
      deadlineAt: r.deadlineAt,
      ...(r.performerName ? { performer: { name: r.performerName } } : {}),
      ...(includeIsOverdue && r.isOverdue !== undefined ? { isOverdue: r.isOverdue } : {}),
      ...(r.domaId ? { domaId: r.domaId } : {}),
    }));

    res.json(apps);
  } catch (err) {
    console.error('Error fetching applications:', err);
    res.status(500).json({ error: 'Failed to fetch applications' });
  }
});

// GET /api/applications/:id -> одна заявка (карточка)
app.get(`${API_PREFIX}/applications/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await pool.query('SELECT * FROM applications WHERE id = $1 LIMIT 1', [id]);
    if (!rows.rows || rows.rows.length === 0) {
      return res.status(404).json({ error: 'Заявка не найдена' });
    }
    const r = rows.rows[0];
    const app = {
      id: r.id,
      number: r.number,
      status: r.status,
      description: r.description || '',
      address: r.address || '',
      apartment: r.apartment || '',
      clientName: r.client_name || '',
      createdAt: r.created_at,
      deadlineAt: r.deadline_at,
      ...(r.performer_name ? { performer: { name: r.performer_name } } : {}),
      ...(r.is_overdue !== undefined ? { isOverdue: r.is_overdue } : {}),
      ...(r.doma_id ? { domaId: r.doma_id } : {}),
      ...(r.source ? { source: r.source } : {}),
      ...(r.source_channel != null ? { sourceChannel: r.source_channel } : {}),
      ...(r.is_from_resident != null ? { isFromResident: r.is_from_resident } : {}),
      ...(r.contact_phone != null ? { contactPhone: r.contact_phone } : {}),
      ...(r.contact_name != null ? { contactName: r.contact_name } : {}),
      ...(r.place_incident != null ? { placeIncident: r.place_incident } : {}),
      ...(r.work_type != null ? { workType: r.work_type } : {}),
      ...(r.problem_detail != null ? { problemDetail: r.problem_detail } : {}),
      ...(r.is_emergency != null ? { isEmergency: r.is_emergency } : {}),
      ...(r.is_paid != null ? { isPaid: r.is_paid } : {}),
      ...(r.is_warranty != null ? { isWarranty: r.is_warranty } : {}),
      ...(r.executor_name != null ? { executorName: r.executor_name } : {}),
      ...(r.responsible_name != null ? { responsibleName: r.responsible_name } : {}),
      ...(r.observers_text != null ? { observersText: r.observers_text } : {}),
      ...(r.show_in_app != null ? { showInApp: r.show_in_app } : {}),
      ...(r.building_id ? { buildingId: r.building_id } : {}),
      ...(r.updated_at ? { updatedAt: r.updated_at } : {}),
    };
    res.json(app);
  } catch (err) {
    console.error('Error fetching application:', err);
    res.status(500).json({ error: err.message || 'Failed to fetch application' });
  }
});

// PATCH /api/applications/:id -> обновление заявки (статус, срок, исполнитель и т.д.) + история
app.patch(`${API_PREFIX}/applications/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const body = req.body || {};
    const changedBy = body.changedBy || 'Администратор';

    const current = await pool.query('SELECT * FROM applications WHERE id = $1 LIMIT 1', [id]);
    if (!current.rows || current.rows.length === 0) {
      return res.status(404).json({ error: 'Заявка не найдена' });
    }
    const row = current.rows[0];

    const logHistory = async (fieldName, oldVal, newVal) => {
      if (String(oldVal) === String(newVal)) return;
      try {
        await pool.query(
          `INSERT INTO application_history (application_id, changed_by, field_name, old_value, new_value)
           VALUES ($1, $2, $3, $4, $5)`,
          [id, changedBy, fieldName, oldVal != null ? String(oldVal) : null, newVal != null ? String(newVal) : null]
        );
      } catch (e) {
        console.warn('[application_history]', e.message);
      }
    };

    const updates = [];
    const values = [];
    let idx = 1;

    // Отмена и отложение — только с причиной; отложение — ещё и с новой датой
    const statusReason = (body.statusReason || body.status_reason || '').trim();
    if (body.status !== undefined && body.status !== row.status) {
      if (body.status === 'canceled') {
        if (!statusReason) {
          return res.status(400).json({ error: 'Укажите причину отмены заявки' });
        }
        updates.push(`status = $${idx}`);
        values.push(body.status);
        idx++;
        await logHistory('status_change', row.status, `Заявка отменена. Причина: ${statusReason}`);
      } else if (body.status === 'deferred') {
        if (!statusReason) {
          return res.status(400).json({ error: 'Укажите причину отложения заявки' });
        }
        if (!body.deadlineAt) {
          return res.status(400).json({ error: 'Укажите новую дату переноса заявки' });
        }
        const newDeadline = new Date(body.deadlineAt).toISOString();
        const oldDeadlineStr = row.deadline_at
          ? new Date(row.deadline_at).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' })
          : '—';
        const newDeadlineStr = new Date(body.deadlineAt).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
        updates.push(`status = $${idx}`);
        values.push(body.status);
        idx++;
        updates.push(`deadline_at = $${idx}`);
        values.push(body.deadlineAt);
        idx++;
        await logHistory('status_change', row.status, `Заявка отложена (с ${oldDeadlineStr} до ${newDeadlineStr}) по причине ${statusReason}`);
      } else {
        await logHistory('status', row.status, body.status);
        updates.push(`status = $${idx}`);
        values.push(body.status);
        idx++;
      }
    }
    if (body.deadlineAt !== undefined && body.status !== 'deferred') {
      const newDeadline = new Date(body.deadlineAt).toISOString();
      const oldDeadline = row.deadline_at ? new Date(row.deadline_at).toISOString() : null;
      if (oldDeadline !== newDeadline) await logHistory('deadline_at', oldDeadline, newDeadline);
      updates.push(`deadline_at = $${idx}`);
      values.push(body.deadlineAt);
      idx++;
    }
    if (body.executorName !== undefined) {
      if (String(row.performer_name || '') !== String(body.executorName || '')) await logHistory('executor_name', row.performer_name, body.executorName);
      updates.push(`performer_name = $${idx}`);
      values.push(body.executorName || null);
      idx++;
    }
    if (body.responsibleName !== undefined) {
      if (row.responsible_name !== body.responsibleName) await logHistory('responsible_name', row.responsible_name, body.responsibleName);
      updates.push(`responsible_name = $${idx}`);
      values.push(body.responsibleName || null);
      idx++;
    }
    if (body.observersText !== undefined) {
      updates.push(`observers_text = $${idx}`);
      values.push(body.observersText || null);
      idx++;
    }
    if (body.address !== undefined) {
      updates.push(`address = $${idx}`);
      values.push(body.address);
      idx++;
    }
    if (body.buildingId !== undefined) {
      updates.push(`building_id = $${idx}`);
      values.push(body.buildingId || null);
      idx++;
    }
    if (body.apartment !== undefined) {
      updates.push(`apartment = $${idx}`);
      values.push(body.apartment || null);
      idx++;
    }
    if (body.contactName !== undefined) {
      updates.push(`contact_name = $${idx}`);
      values.push(body.contactName || null);
      idx++;
    }
    if (body.contactPhone !== undefined) {
      updates.push(`contact_phone = $${idx}`);
      values.push(body.contactPhone || null);
      idx++;
    }
    if (body.description !== undefined) {
      updates.push(`description = $${idx}`);
      values.push(body.description);
      idx++;
    }

    if (updates.length === 0) {
      const rows = await pool.query('SELECT * FROM applications WHERE id = $1 LIMIT 1', [id]);
      const r = rows.rows[0];
      return res.json(mapApplicationRow(r));
    }

    updates.push(`updated_at = NOW()`);
    values.push(id);
    await pool.query(
      `UPDATE applications SET ${updates.join(', ')} WHERE id = $${idx}`,
      values
    );
    const updated = await pool.query('SELECT * FROM applications WHERE id = $1 LIMIT 1', [id]);
    const appRow = updated.rows[0];
    try {
      const userIds = [];
      if (appRow.employee_id) {
        const uid = await notificationService.resolveEmployeeIdToUserId(pool, appRow.employee_id);
        if (uid) userIds.push(uid);
      }
      const names = [appRow.performer_name, appRow.responsible_name].filter(Boolean);
      if (names.length) {
        const byName = await notificationService.resolveEmployeeNamesToUserIds(pool, names);
        userIds.push(...byName);
      }
      const uniqueUserIds = [...new Set(userIds)];
      const title = body.status !== undefined ? `Заявка №${appRow.number}: смена статуса` : body.deadlineAt ? `Заявка №${appRow.number}: изменён срок` : `Заявка №${appRow.number}: обновление`;
      const bodyText = body.status !== undefined ? `Статус: ${body.status}` : body.deadlineAt ? `Новый срок: ${new Date(body.deadlineAt).toLocaleDateString('ru-RU')}` : 'Данные заявки изменены';
      const opts = { type: 'application_update', title, body: bodyText, entityType: 'application', entityId: String(id) };
      if (uniqueUserIds.length > 0) {
        await notificationService.createNotificationForUserIds(pool, uniqueUserIds, opts);
      } else {
        await notificationService.createNotificationForResponsibleZone(pool, 'requests', 'registry', opts);
      }
    } catch (notifErr) {
      console.warn('[notifications] application PATCH:', notifErr.message);
    }
    res.json(mapApplicationRow(appRow));
  } catch (err) {
    console.error('Error updating application:', err);
    res.status(500).json({ error: err.message || 'Failed to update application' });
  }
});

function mapApplicationRow(r) {
  if (!r) return null;
  return {
    id: r.id,
    number: r.number,
    status: r.status,
    description: r.description || '',
    address: r.address || '',
    apartment: r.apartment || '',
    clientName: r.client_name || '',
    createdAt: r.created_at,
    deadlineAt: r.deadline_at,
    ...(r.performer_name ? { performer: { name: r.performer_name } } : {}),
    ...(r.is_overdue !== undefined ? { isOverdue: r.is_overdue } : {}),
    ...(r.doma_id ? { domaId: r.doma_id } : {}),
    ...(r.source ? { source: r.source } : {}),
    ...(r.source_channel != null ? { sourceChannel: r.source_channel } : {}),
    ...(r.contact_phone != null ? { contactPhone: r.contact_phone } : {}),
    ...(r.contact_name != null ? { contactName: r.contact_name } : {}),
    ...(r.place_incident != null ? { placeIncident: r.place_incident } : {}),
    ...(r.work_type != null ? { workType: r.work_type } : {}),
    ...(r.problem_detail != null ? { problemDetail: r.problem_detail } : {}),
    ...(r.executor_name != null ? { executorName: r.executor_name } : {}),
    ...(r.responsible_name != null ? { responsibleName: r.responsible_name } : {}),
    ...(r.observers_text != null ? { observersText: r.observers_text } : {}),
  };
}

// GET /api/applications/:id/history -> история изменений заявки
app.get(`${API_PREFIX}/applications/:id/history`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await pool.query(
      `SELECT id, application_id AS "applicationId", changed_by AS "changedBy", changed_at AS "changedAt",
              field_name AS "fieldName", old_value AS "oldValue", new_value AS "newValue"
       FROM application_history WHERE application_id = $1 ORDER BY changed_at DESC`,
      [id]
    );
    res.json(rows.rows || []);
  } catch (err) {
    console.error('Error fetching application history:', err);
    res.status(500).json({ error: err.message || 'Failed to fetch history' });
  }
});

// GET /api/applications/:id/comments -> комментарии (type: internal | resident)
app.get(`${API_PREFIX}/applications/:id/comments`, async (req, res) => {
  try {
    const { id } = req.params;
    const { type } = req.query;
    let sql = `SELECT id, application_id AS "applicationId", author_name AS "authorName", type, text, created_at AS "createdAt"
               FROM application_comments WHERE application_id = $1`;
    const params = [id];
    if (type === 'internal' || type === 'resident') {
      sql += ' AND type = $2';
      params.push(type);
    }
    sql += ' ORDER BY created_at DESC';
    const rows = await pool.query(sql, params);
    res.json(rows.rows || []);
  } catch (err) {
    if (err.message && err.message.includes('application_comments')) {
      return res.json([]);
    }
    console.error('Error fetching comments:', err);
    res.status(500).json({ error: err.message || 'Failed to fetch comments' });
  }
});

// POST /api/applications/:id/comments -> добавить комментарий (internal — без жителя)
app.post(`${API_PREFIX}/applications/:id/comments`, async (req, res) => {
  try {
    const { id } = req.params;
    const body = req.body || {};
    const text = (body.text || '').trim();
    const type = body.type === 'resident' ? 'resident' : 'internal';
    const authorName = body.authorName || body.author_name || 'Администратор';
    if (!text) {
      return res.status(400).json({ error: 'Текст комментария обязателен' });
    }
    const rows = await pool.query(
      `INSERT INTO application_comments (application_id, author_name, type, text)
       VALUES ($1, $2, $3, $4)
       RETURNING id, application_id AS "applicationId", author_name AS "authorName", type, text, created_at AS "createdAt"`,
      [id, authorName, type, text]
    );
    try {
      const appRows = await pool.query('SELECT number, employee_id, performer_name, responsible_name FROM applications WHERE id = $1', [id]);
      if (appRows.rows.length > 0) {
        const appRow = appRows.rows[0];
        const userIds = [];
        if (appRow.employee_id) {
          const uid = await notificationService.resolveEmployeeIdToUserId(pool, appRow.employee_id);
          if (uid) userIds.push(uid);
        }
        const names = [appRow.performer_name, appRow.responsible_name].filter(Boolean);
        if (names.length) {
          const byName = await notificationService.resolveEmployeeNamesToUserIds(pool, names);
          userIds.push(...byName);
        }
        const uniqueUserIds = [...new Set(userIds)];
        const opts = { type: 'application_comment', title: `Заявка №${appRow.number}: новый комментарий`, body: text.slice(0, 80) + (text.length > 80 ? '…' : ''), entityType: 'application', entityId: String(id) };
        if (uniqueUserIds.length > 0) {
          await notificationService.createNotificationForUserIds(pool, uniqueUserIds, opts);
        } else {
          await notificationService.createNotificationForResponsibleZone(pool, 'requests', 'registry', opts);
        }
      }
    } catch (notifErr) {
      console.warn('[notifications] application comment:', notifErr.message);
    }
    res.status(201).json(rows.rows[0]);
  } catch (err) {
    if (err.message && err.message.includes('application_comments')) {
      return res.status(503).json({ error: 'Таблица комментариев ещё не создана. Перезапустите сервер.' });
    }
    console.error('Error adding comment:', err);
    res.status(500).json({ error: err.message || 'Failed to add comment' });
  }
});

// POST /api/applications -> создание заявки вручную (диспетчерская)
app.post(`${API_PREFIX}/applications`, async (req, res) => {
  try {
    const body = req.body || {};
    const address = (body.address || '').trim();
    const description = (body.description || '').trim();
    if (!address || !description) {
      return res.status(400).json({ error: 'Обязательные поля: address, description' });
    }
    const deadlineAt = body.deadlineAt
      ? new Date(body.deadlineAt)
      : (() => { const d = new Date(); d.setDate(d.getDate() + 8); return d; })();
    const number = 'M-' + Date.now();
    const clientName = (body.contactName || body.clientName || '').trim() || 'Не указан';
    const apartment = (body.apartment || '').trim() || '';
    const performerName = body.executorName || null;

    // Сначала пробуем полный INSERT (с source и полями карточки)
    let inserted = false;
    const fullParams = [
      number, description, address, apartment, clientName, deadlineAt,
      performerName,
      body.sourceChannel || null,
      body.isFromResident !== false,
      body.contactPhone || null,
      body.contactName || null,
      body.placeIncident || null,
      body.workType || null,
      body.problemDetail || null,
      !!body.isEmergency,
      !!body.isPaid,
      !!body.isWarranty,
      body.executorName || null,
      body.responsibleName || null,
      body.observersText || null,
      !!body.showInApp,
      body.buildingId || null
    ];

    try {
      const fullRes = await pool.query(
        `INSERT INTO applications (
          number, status, description, address, apartment, client_name, created_at, deadline_at,
          performer_name, source, source_channel, is_from_resident, contact_phone, contact_name,
          place_incident, work_type, problem_detail, is_emergency, is_paid, is_warranty,
          executor_name, responsible_name, observers_text, show_in_app, building_id
        ) VALUES (
          $1, 'new', $2, $3, $4, $5, NOW(), $6, $7, 'manual', $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22
        ) RETURNING id, number, status, description, address, apartment, client_name AS "clientName",
          created_at AS "createdAt", deadline_at AS "deadlineAt", performer_name AS "performerName"`,
        fullParams
      );
      if (fullRes.rows && fullRes.rows.length > 0) {
        inserted = true;
        const row = fullRes.rows[0];
        try {
          const names = [performerName, body.responsibleName].filter(Boolean);
          const userIds = names.length ? await notificationService.resolveEmployeeNamesToUserIds(pool, names) : [];
          const opts = { type: 'application_new', title: `Новая заявка №${row.number}`, body: description.slice(0, 80) + (description.length > 80 ? '…' : ''), entityType: 'application', entityId: String(row.id) };
          if (userIds.length > 0) {
            await notificationService.createNotificationForUserIds(pool, userIds, opts);
          } else {
            await notificationService.createNotificationForResponsibleZone(pool, 'requests', 'registry', opts);
          }
        } catch (notifErr) {
          console.warn('[notifications] application POST:', notifErr.message);
        }
        const app = {
          id: row.id,
          number: row.number,
          status: row.status,
          description: row.description,
          address: row.address,
          apartment: row.apartment,
          clientName: row.clientName,
          createdAt: row.createdAt,
          deadlineAt: row.deadlineAt,
          ...(row.performerName ? { performer: { name: row.performerName } } : {}),
        };
        return res.status(201).json(app);
      }
    } catch (fullErr) {
      console.warn('[POST /applications] Full INSERT failed (missing columns?), fallback to minimal:', fullErr.message);
    }

    // Fallback: минимальный INSERT (только колонки из базовой схемы)
    if (!inserted) {
      const minRes = await pool.query(
        `INSERT INTO applications (number, status, description, address, apartment, client_name, created_at, deadline_at, performer_name)
         VALUES ($1, 'new', $2, $3, $4, $5, NOW(), $6, $7)
         RETURNING id, number, status, description, address, apartment, client_name AS "clientName",
           created_at AS "createdAt", deadline_at AS "deadlineAt", performer_name AS "performerName"`,
        [number, description, address, apartment, clientName, deadlineAt, performerName]
      );
      if (!minRes.rows || minRes.rows.length === 0) {
        return res.status(500).json({ error: 'Не удалось создать заявку в БД' });
      }
      const row = minRes.rows[0];
      try {
        const names = [performerName].filter(Boolean);
        const userIds = names.length ? await notificationService.resolveEmployeeNamesToUserIds(pool, names) : [];
        const opts = { type: 'application_new', title: `Новая заявка №${row.number}`, body: description.slice(0, 80) + (description.length > 80 ? '…' : ''), entityType: 'application', entityId: String(row.id) };
        if (userIds.length > 0) {
          await notificationService.createNotificationForUserIds(pool, userIds, opts);
        } else {
          await notificationService.createNotificationForResponsibleZone(pool, 'requests', 'registry', opts);
        }
      } catch (notifErr) {
        console.warn('[notifications] application POST:', notifErr.message);
      }
      const app = {
        id: row.id,
        number: row.number,
        status: row.status,
        description: row.description,
        address: row.address,
        apartment: row.apartment,
        clientName: row.clientName,
        createdAt: row.createdAt,
        deadlineAt: row.deadlineAt,
        ...(row.performerName ? { performer: { name: row.performerName } } : {}),
      };
      return res.status(201).json(app);
    }
  } catch (err) {
    console.error('Error creating application:', err);
    res.status(500).json({ error: err.message || 'Failed to create application' });
  }
});

// ====== СИНХРОНИЗАЦИЯ ЗАЯВОК ИЗ DOMA AI ======

/**
 * Нормализация адреса для сравнения (убирает лишние пробелы, приводит к нижнему регистру, убирает сокращения)
 */
function normalizeAddress(address) {
  if (!address) return '';
  return address
    .toLowerCase()
    .trim()
    .replace(/\s+/g, ' ') // Множественные пробелы в один
    .replace(/г\./g, 'г ') // г. -> г
    .replace(/ул\./g, 'ул ') // ул. -> ул
    .replace(/д\./g, 'д ') // д. -> д
    .replace(/пр\./g, 'пр ') // пр. -> пр
    .replace(/[.,]/g, '') // Убираем точки и запятые
    .trim();
}

/**
 * Нормализация имени для сравнения
 */
function normalizeName(name) {
  if (!name) return '';
  return name
    .toLowerCase()
    .trim()
    .replace(/\s+/g, ' ') // Множественные пробелы в один
    .trim();
}

/**
 * Поиск похожего дома по нормализованному адресу
 */
async function findSimilarBuilding(pool, domaAddress) {
  const normalized = normalizeAddress(domaAddress);
  
  // Сначала проверяем сохранённые сопоставления
  const mappingResult = await pool.query(
    `SELECT building_id FROM doma_address_mappings WHERE LOWER(TRIM(doma_address)) = LOWER(TRIM($1)) LIMIT 1`,
    [domaAddress]
  );
  
  if (mappingResult.rows.length > 0) {
    return mappingResult.rows[0].building_id;
  }
  
  // Ищем по точному совпадению нормализованного адреса
  try {
    const exactResult = await pool.query(
      `SELECT id, data->'passport'->>'address' as address 
       FROM buildings 
       WHERE normalize_address(COALESCE(data->'passport'->>'address', '')) = normalize_address($1) 
       LIMIT 1`,
      [domaAddress]
    );
    
    if (exactResult.rows.length > 0) {
      return exactResult.rows[0].id;
    }
  } catch (err) {
    // Если функция normalize_address не существует, используем простой поиск
    const exactResult = await pool.query(
      `SELECT id FROM buildings 
       WHERE LOWER(TRIM(COALESCE(data->'passport'->>'address', ''))) = LOWER(TRIM($1)) 
       LIMIT 1`,
      [domaAddress]
    );
    
    if (exactResult.rows.length > 0) {
      return exactResult.rows[0].id;
    }
  }
  
  // Ищем по частичному совпадению (извлекаем ключевые слова: улица и номер дома)
  const addressParts = normalized.match(/(?:ул|улица|проспект|пр|проезд|переулок|пер)\s+([^,\s]+).*?(?:д|дом)\s*(\d+)/);
  if (addressParts && addressParts.length >= 3) {
    const street = addressParts[1];
    const houseNumber = addressParts[2];
    
    try {
      const similarResult = await pool.query(
        `SELECT id, data->'passport'->>'address' as address 
         FROM buildings 
         WHERE normalize_address(COALESCE(data->'passport'->>'address', '')) LIKE $1 
           AND normalize_address(COALESCE(data->'passport'->>'address', '')) LIKE $2
         LIMIT 5`,
        [`%${street}%`, `%д ${houseNumber}%`]
      );
      
      if (similarResult.rows.length > 0) {
        // Возвращаем первый результат (самый похожий)
        return similarResult.rows[0].id;
      }
    } catch (err) {
      // Fallback на простой поиск
      const similarResult = await pool.query(
        `SELECT id FROM buildings 
         WHERE LOWER(COALESCE(data->'passport'->>'address', '')) LIKE $1 
           AND LOWER(COALESCE(data->'passport'->>'address', '')) LIKE $2
         LIMIT 5`,
        [`%${street}%`, `%д ${houseNumber}%`]
      );
      
      if (similarResult.rows.length > 0) {
        return similarResult.rows[0].id;
      }
    }
  }
  
  return null;
}

/**
 * Поиск похожего сотрудника по нормализованному имени
 */
async function findSimilarEmployee(pool, domaName) {
  const normalized = normalizeName(domaName);
  
  // Сначала проверяем сохранённые сопоставления
  const mappingResult = await pool.query(
    `SELECT employee_id FROM doma_employee_mappings WHERE LOWER(TRIM(doma_name)) = LOWER(TRIM($1)) LIMIT 1`,
    [domaName]
  );
  
  if (mappingResult.rows.length > 0) {
    return mappingResult.rows[0].employee_id;
  }
  
  // Ищем по точному совпадению нормализованного имени
  try {
    const exactResult = await pool.query(
      `SELECT id FROM employees WHERE normalize_name(name) = normalize_name($1) LIMIT 1`,
      [domaName]
    );
    
    if (exactResult.rows.length > 0) {
      return exactResult.rows[0].id;
    }
  } catch (err) {
    // Если функция normalize_name не существует, используем простой поиск
    const exactResult = await pool.query(
      `SELECT id FROM employees WHERE LOWER(TRIM(name)) = LOWER(TRIM($1)) LIMIT 1`,
      [domaName]
    );
    
    if (exactResult.rows.length > 0) {
      return exactResult.rows[0].id;
    }
  }
  
  // Ищем по частичному совпадению (первые слова имени)
  const nameWords = normalized.split(' ').filter(w => w.length > 2);
  if (nameWords.length > 0) {
    const firstWord = nameWords[0];
    const similarResult = await pool.query(
      `SELECT id, name FROM employees 
       WHERE LOWER(name) LIKE $1 
       LIMIT 5`,
      [`%${firstWord}%`]
    );
    
    if (similarResult.rows.length > 0) {
      // Возвращаем первый результат
      return similarResult.rows[0].id;
    }
  }
  
  return null;
}

/**
 * Маппинг статуса из Doma AI в наш enum doma_application_status
 * Ориентируемся на поле type (английские значения)
 */
function mapDomaStatusToLocal(type, name) {
  const statusType = (type || '').toString().trim().toLowerCase();

  // Если ничего не пришло – считаем новой
  if (!statusType) {
    console.warn(`[mapDomaStatusToLocal] Пустой type, возвращаем 'new'. name: ${name}`);
    return 'new';
  }

  // Маппинг по типу статуса из Doma AI (точные совпадения сначала)
  
  // canceled → canceled (проверяем первым, чтобы не попасть в другие категории)
  if (statusType === 'canceled' || statusType === 'cancelled') {
    return 'canceled';
  }

  // completed → done
  if (statusType === 'completed') {
    return 'done';
  }

  // closed → done
  if (statusType === 'closed') {
    return 'done';
  }

  // processing → in_progress
  if (statusType === 'processing') {
    return 'in_progress';
  }

  // deferred → deferred (отложена)
  if (statusType === 'deferred') {
    return 'deferred';
  }

  // new_or_reopened → new (точное совпадение)
  if (statusType === 'new_or_reopened') {
    return 'new';
  }

  // Fallback: если type содержит ключевые слова (более общие проверки)
  if (statusType.includes('cancel')) {
    return 'canceled';
  }
  if (statusType.includes('done') || statusType.includes('complete')) {
    return 'done';
  }
  if (statusType.includes('progress') || statusType.includes('work')) {
    return 'in_progress';
  }
  if (statusType.includes('new')) {
    return 'new';
  }

  // По умолчанию считаем "в работе", чтобы не терять активные
  console.warn(`[mapDomaStatusToLocal] Неизвестный тип статуса: "${type}" (name: "${name}"), возвращаем 'in_progress'`);
  return 'in_progress';
}

/**
 * Синхронизация заявок из Doma AI в таблицу applications
 */
async function syncDomaApplications() {
  const { apiUrl, token } = await getDomaSettingsFromDb();

  if (!apiUrl || !token) {
    console.warn('[backend] Настройки Дома.АИ (apiUrl/токен) не заданы в панели интеграций или в env, синхронизация пропущена');
    return { synced: 0 };
  }

  console.log('[backend] Запуск синхронизации заявок из Doma AI…');

  const queryText = `
    query GetTickets {
      objs: allTickets {
        id
        number
        status {
          id
          name
          type
        }
        details
        createdAt
        updatedAt
        deadline
        property {
          id
          address
        }
        client {
          id
          name
        }
        contact {
          id
          name
          email
          phone
        }
        assignee {
          id
          name
        }
      }
    }
  `;

  try {
    const response = await axios.post(
      apiUrl,
      { query: queryText, variables: {} },
      {
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${token}`,
        },
      }
    );

    const data = response.data;

    if (data.errors && data.errors.length) {
      console.error('[backend] Ошибка Doma AI при получении заявок:', data.errors);
      throw new Error(data.errors[0].message || 'GraphQL error from Doma AI');
    }

    const tickets = (data.data && data.data.objs) || [];

    console.log(`[backend] Получено ${tickets.length} заявок из Doma AI, начинаем запись в БД…`);

    await pool.query('BEGIN');

    // Проверяем наличие новых колонок один раз перед циклом
    const hasNewColumns = await pool.query(`
      SELECT column_name 
      FROM information_schema.columns 
      WHERE table_name = 'applications' 
      AND column_name IN ('building_id', 'employee_id', 'is_overdue', 'updated_at', 'doma_id')
    `);
    
    const hasBuildingId = hasNewColumns.rows.some(r => r.column_name === 'building_id');
    const hasEmployeeId = hasNewColumns.rows.some(r => r.column_name === 'employee_id');
    const hasIsOverdue = hasNewColumns.rows.some(r => r.column_name === 'is_overdue');
    const hasUpdatedAt = hasNewColumns.rows.some(r => r.column_name === 'updated_at');
    const hasDomaId = hasNewColumns.rows.some(r => r.column_name === 'doma_id');
    
    // Добавляем колонку doma_id если её нет
    if (!hasDomaId) {
      try {
        await pool.query(`ALTER TABLE applications ADD COLUMN IF NOT EXISTS doma_id VARCHAR(255)`);
        console.log('[syncDomaApplications] Добавлена колонка doma_id');
      } catch (err) {
        console.warn('[syncDomaApplications] Ошибка при добавлении колонки doma_id:', err.message);
      }
    }

    // Удаляем только заявки из Doma AI (doma_id IS NOT NULL), ручные заявки (source=manual, doma_id=NULL) не трогаем
    try {
      const hasDomaIdNow = await pool.query(`
        SELECT 1 FROM information_schema.columns WHERE table_name = 'applications' AND column_name = 'doma_id'
      `);
      if (hasDomaIdNow.rows.length > 0) {
        await pool.query(`DELETE FROM applications WHERE doma_id IS NOT NULL`);
        console.log('[syncDomaApplications] Удалены только заявки из Doma AI, ручные заявки сохранены');
      } else {
        await pool.query('DELETE FROM applications');
      }
    } catch (delErr) {
      await pool.query('DELETE FROM applications');
    }
    
    const useNewColumns = hasBuildingId && hasEmployeeId && hasIsOverdue && hasUpdatedAt;

    if (!useNewColumns) {
      console.warn('[syncDomaApplications] Новые колонки (building_id, is_overdue, updated_at) не найдены. Используется старый формат. Выполните миграцию БД через dbInit.');
    }

    const now = new Date();
    let overdueCount = 0;
    let buildingsLinked = 0;
    let employeesLinked = 0;

    for (const ticket of tickets) {
      // Передаём type и name в функцию маппинга (ориентируемся на type)
      const localStatus = mapDomaStatusToLocal(ticket.status?.type, ticket.status?.name);
      
      // Логируем для отладки
      console.log(`[syncDomaApplications] Заявка #${ticket.number}: type="${ticket.status?.type}", name="${ticket.status?.name}" → localStatus="${localStatus}"`);

      const number = (ticket.number != null ? String(ticket.number) : ticket.id) || '';
      const description = ticket.details || 'Без описания';
      // Используем данные напрямую из ответа Doma AI
      const address = ticket.property?.address || 'Адрес не указан';
      const apartment = '—';
      // Используем contact.name если есть, иначе client.name
      const clientName = ticket.contact?.name || ticket.client?.name || 'Клиент не указан';
      const domaId = ticket.id; // Сохраняем ID из Doma.AI для формирования ссылки
      const createdAt = ticket.createdAt || new Date().toISOString();
      const deadlineAt =
        ticket.deadline || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
      // Используем данные напрямую из ответа Doma AI: assignee.name
      const performerName = ticket.assignee?.name || null;

      // Ищем сотрудника по имени из assignee.name (данные из Doma AI)
      let employeeId = null;
      if (performerName && performerName !== 'Клиент не указан') {
        try {
          const performerTrimmed = performerName.trim();
          
          // Используем функцию поиска похожих записей
          const similarEmployeeId = await findSimilarEmployee(pool, performerTrimmed);
          
          if (similarEmployeeId) {
            employeeId = similarEmployeeId;
            // Сохраняем сопоставление, если его ещё нет
            await pool.query(
              `INSERT INTO doma_employee_mappings (doma_name, employee_id)
               VALUES ($1, $2)
               ON CONFLICT (doma_name) DO NOTHING`,
              [performerTrimmed, employeeId]
            );
          } else {
            // Не создаём сотрудника автоматически — добавляем в ожидающие сопоставления
            try {
              const existingPending = await pool.query(
                `SELECT id FROM pending_doma_mappings 
                 WHERE type = 'employee' AND doma_value = $1 AND resolved_at IS NULL 
                 LIMIT 1`,
                [performerTrimmed]
              );

              if (existingPending.rows.length === 0) {
                await pool.query(
                  `INSERT INTO pending_doma_mappings (type, doma_value)
                   VALUES ('employee', $1)`,
                  [performerTrimmed]
                );
                console.log(`[syncDomaApplications] Добавлена ожидающая запись сопоставления сотрудника "${performerTrimmed}"`);
              }
            } catch (pendingErr) {
              console.warn(
                `[syncDomaApplications] Ошибка при добавлении ожидающего сопоставления сотрудника "${performerTrimmed}":`,
                pendingErr.message
              );
            }
          }
        } catch (err) {
          console.warn(`[syncDomaApplications] Ошибка обработки сотрудника "${performerName}":`, err.message);
        }
      }

      // Проверяем просрочку: заявка просрочена, если deadline прошёл и статус не 'done' и не 'canceled'
      const deadlineDate = new Date(deadlineAt);
      const isOverdue = deadlineDate < now && 
                        localStatus !== 'done' && 
                        localStatus !== 'canceled';
      
      if (isOverdue) {
        overdueCount++;
      }

      // Ищем дом по адресу из property.address (данные из Doma AI)
      let buildingId = null;
      if (address && address !== 'Адрес не указан') {
        try {
          const addressTrimmed = address.trim();
          
          // Используем функцию поиска похожих записей
          const similarBuildingId = await findSimilarBuilding(pool, addressTrimmed);
          
          if (similarBuildingId) {
            buildingId = similarBuildingId;
            buildingsLinked++;
            // Сохраняем сопоставление, если его ещё нет
            await pool.query(
              `INSERT INTO doma_address_mappings (doma_address, building_id)
               VALUES ($1, $2)
               ON CONFLICT (doma_address) DO NOTHING`,
              [addressTrimmed, buildingId]
            );
          } else {
            // Не создаём дом автоматически — добавляем в ожидающие сопоставления
            try {
              const existingPending = await pool.query(
                `SELECT id FROM pending_doma_mappings 
                 WHERE type = 'building' AND doma_value = $1 AND resolved_at IS NULL 
                 LIMIT 1`,
                [addressTrimmed]
              );

              if (existingPending.rows.length === 0) {
                await pool.query(
                  `INSERT INTO pending_doma_mappings (type, doma_value)
                   VALUES ('building', $1)`,
                  [addressTrimmed]
                );
                console.log(`[syncDomaApplications] Добавлена ожидающая запись сопоставления дома "${addressTrimmed}"`);
              }
            } catch (pendingErr) {
              console.warn(
                `[syncDomaApplications] Ошибка при добавлении ожидающего сопоставления дома "${addressTrimmed}":`,
                pendingErr.message
              );
            }
          }
        } catch (err) {
          console.warn(`[syncDomaApplications] Ошибка обработки дома "${address}":`, err.message);
        }
      }

      // Используем соответствующий формат INSERT в зависимости от наличия колонок
      if (useNewColumns) {
        await pool.query(
          `INSERT INTO applications
            (number, status, description, address, apartment, client_name, created_at, deadline_at, performer_name, employee_id, building_id, is_overdue, updated_at, doma_id)
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`,
          [
            number,
            localStatus,
            description,
            address,
            apartment,
            clientName,
            createdAt,
            deadlineAt,
            performerName,
            employeeId,
            buildingId,
            isOverdue,
            now.toISOString(),
            domaId,
          ]
        );
      } else {
        // Используем старый формат, но добавляем doma_id если колонка есть
        if (hasDomaId) {
          await pool.query(
            `INSERT INTO applications
              (number, status, description, address, apartment, client_name, created_at, deadline_at, performer_name, doma_id)
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
            [
              number,
              localStatus,
              description,
              address,
              apartment,
              clientName,
              createdAt,
              deadlineAt,
              performerName,
              domaId,
            ]
          );
        } else {
          await pool.query(
            `INSERT INTO applications
              (number, status, description, address, apartment, client_name, created_at, deadline_at, performer_name)
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
            [
              number,
              localStatus,
              description,
              address,
              apartment,
              clientName,
              createdAt,
              deadlineAt,
              performerName,
            ]
          );
        }
      }
    }

    await pool.query('COMMIT');
    
    console.log(`[backend] Синхронизация завершена: ${tickets.length} заявок, ${overdueCount} просрочено, ${buildingsLinked} связано с домами, ${employeesLinked} связано с сотрудниками`);

    console.log('[backend] Синхронизация заявок из Doma AI завершена успешно');

    // После синхронизации пересчитываем статистику производительности
    try {
      await calculatePerformanceStats();
      console.log('[backend] Статистика производительности пересчитана');
    } catch (statsErr) {
      console.warn('[backend] Ошибка при расчёте статистики производительности:', statsErr.message);
      // Не прерываем выполнение, т.к. это не критично
    }

    return { synced: tickets.length, overdue: overdueCount, buildingsLinked, employeesLinked };
  } catch (err) {
    await pool.query('ROLLBACK').catch(() => {});
    console.error('[backend] Ошибка синхронизации заявок из Doma AI:', err.message || err);
    throw err;
  }
}

/**
 * Расчёт статистики производительности сотрудников и участков
 * Учитывает общий контекст для справедливого рейтинга
 */
async function calculatePerformanceStats() {
  const now = new Date();
  const periodStart = new Date(now.getFullYear(), now.getMonth(), 1); // Начало текущего месяца
  const periodEnd = now;

  console.log('[calculatePerformanceStats] Начинаем расчёт статистики производительности...');

  try {
    // Получаем общую статистику по всем заявкам за период
    const overallStats = await pool.query(`
      SELECT 
        COUNT(*) as total,
        COUNT(*) FILTER (WHERE status = 'done') as completed,
        COUNT(*) FILTER (WHERE is_overdue = true) as overdue,
        COUNT(*) FILTER (WHERE status IN ('in_progress', 'deferred')) as in_progress
      FROM applications
      WHERE created_at >= $1 AND created_at <= $2
    `, [periodStart, periodEnd]);

    const overall = overallStats.rows[0];
    const totalOverall = parseInt(overall.total) || 0;
    const completedOverall = parseInt(overall.completed) || 0;
    const overdueOverall = parseInt(overall.overdue) || 0;
    const overallCompletionRate = totalOverall > 0 ? (completedOverall / totalOverall) * 100 : 0;

    console.log(`[calculatePerformanceStats] Общая статистика: ${totalOverall} заявок, ${completedOverall} выполнено, ${overdueOverall} просрочено`);

    // Получаем статистику по каждому сотруднику
    // Используем employee_id если есть, иначе performer_name (для обратной совместимости)
    const employeeStats = await pool.query(`
      SELECT 
        COALESCE(e.id, a.performer_name) as employee_id_or_name,
        COALESCE(e.name, a.performer_name) as employee_name,
        e.id as employee_id,
        e.assigned_district_id as district_id,
        COUNT(*) as total_assigned,
        COUNT(*) FILTER (WHERE a.status = 'done') as total_completed,
        COUNT(*) FILTER (WHERE a.is_overdue = true) as total_overdue,
        COUNT(*) FILTER (WHERE a.status = 'in_progress') as total_in_progress,
        COUNT(*) FILTER (WHERE a.status = 'deferred') as total_deferred
      FROM applications a
      LEFT JOIN employees e ON a.employee_id = e.id
      WHERE (a.employee_id IS NOT NULL OR a.performer_name IS NOT NULL)
        AND a.created_at >= $1 AND a.created_at <= $2
      GROUP BY e.id, e.name, e.assigned_district_id, a.performer_name
    `, [periodStart, periodEnd]);

    await pool.query('BEGIN');

    // Очищаем старую статистику за текущий период
    await pool.query(`
      DELETE FROM employee_performance_stats 
      WHERE period_start = $1 AND period_end >= $2
    `, [periodStart, periodStart]);

    // Сохраняем статистику по каждому сотруднику
    for (const emp of employeeStats.rows) {
      const employeeName = emp.employee_name || emp.employee_id_or_name;
      const employeeId = emp.employee_id;
      const totalAssigned = parseInt(emp.total_assigned) || 0;
      const totalCompleted = parseInt(emp.total_completed) || 0;
      const totalOverdue = parseInt(emp.total_overdue) || 0;
      const totalInProgress = parseInt(emp.total_in_progress) || 0;
      const totalDeferred = parseInt(emp.total_deferred) || 0;
      const districtId = emp.district_id;

      // Расчёт процента выполнения с учётом общего контекста
      // Если у сотрудника 3 задачи и все выполнены, но общий процент выполнения 50%,
      // то рейтинг сотрудника будет выше среднего, но не 100%
      let completionRate = 0;
      if (totalAssigned > 0) {
        const employeeCompletionRate = (totalCompleted / totalAssigned) * 100;
        // Учитываем общий контекст: если сотрудник выше среднего, получает бонус
        // Если ниже среднего, получает штраф
        const contextFactor = overallCompletionRate > 0 
          ? (employeeCompletionRate / overallCompletionRate) 
          : 1;
        // Нормализуем: базовый рейтинг + бонус/штраф за контекст
        completionRate = Math.min(100, employeeCompletionRate * (0.7 + 0.3 * Math.min(contextFactor, 1.5)));
      }

      // Процент просрочек
      const overdueRate = totalAssigned > 0 ? (totalOverdue / totalAssigned) * 100 : 0;

      // Общий рейтинг производительности (0-100)
      // Формула: completion_rate * 0.7 - overdue_rate * 0.3
      // Минус штраф за просрочки, плюс бонус за выполнение выше среднего
      let performanceScore = completionRate * 0.7 - overdueRate * 0.3;
      
      // Бонус за выполнение выше среднего
      if (totalAssigned > 0 && totalCompleted > 0) {
        const employeeCompletion = (totalCompleted / totalAssigned) * 100;
        if (employeeCompletion > overallCompletionRate) {
          const bonus = (employeeCompletion - overallCompletionRate) * 0.2;
          performanceScore += bonus;
        }
      }

      // Ограничиваем диапазон 0-100
      performanceScore = Math.max(0, Math.min(100, performanceScore));

      await pool.query(`
        INSERT INTO employee_performance_stats
          (employee_name, period_start, period_end, total_assigned, total_completed, 
           total_overdue, total_in_progress, total_deferred, completion_rate, 
           overdue_rate, performance_score, district_id, calculated_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
        ON CONFLICT (employee_name, period_start, period_end) 
        DO UPDATE SET
          total_assigned = EXCLUDED.total_assigned,
          total_completed = EXCLUDED.total_completed,
          total_overdue = EXCLUDED.total_overdue,
          total_in_progress = EXCLUDED.total_in_progress,
          total_deferred = EXCLUDED.total_deferred,
          completion_rate = EXCLUDED.completion_rate,
          overdue_rate = EXCLUDED.overdue_rate,
          performance_score = EXCLUDED.performance_score,
          district_id = EXCLUDED.district_id,
          updated_at = EXCLUDED.updated_at
      `, [
        employeeName,
        periodStart,
        periodEnd,
        totalAssigned,
        totalCompleted,
        totalOverdue,
        totalInProgress,
        totalDeferred,
        completionRate,
        overdueRate,
        performanceScore,
        districtId,
        now,
        now,
      ]);
    }

    // Расчёт статистики по участкам
    // Используем employee_id если есть, иначе performer_name (для обратной совместимости)
    const districtStats = await pool.query(`
      SELECT 
        d.id as district_id,
        COUNT(a.id) as total_applications,
        COUNT(a.id) FILTER (WHERE a.status = 'done') as total_completed,
        COUNT(a.id) FILTER (WHERE a.is_overdue = true) as total_overdue,
        COUNT(a.id) FILTER (WHERE a.status IN ('in_progress', 'deferred')) as total_in_progress
      FROM districts d
      LEFT JOIN employees e ON e.assigned_district_id = d.id
      LEFT JOIN applications a ON (
        (a.employee_id = e.id) OR 
        (a.employee_id IS NULL AND a.performer_name = e.name)
      )
        AND a.created_at >= $1 AND a.created_at <= $2
      GROUP BY d.id
      HAVING COUNT(a.id) > 0
    `, [periodStart, periodEnd]);

    // Очищаем старую статистику по участкам
    await pool.query(`
      DELETE FROM district_performance_stats 
      WHERE period_start = $1 AND period_end >= $2
    `, [periodStart, periodStart]);

    // Сохраняем статистику по участкам
    for (const dist of districtStats.rows) {
      const districtId = dist.district_id;
      const totalApplications = parseInt(dist.total_applications) || 0;
      const totalCompleted = parseInt(dist.total_completed) || 0;
      const totalOverdue = parseInt(dist.total_overdue) || 0;
      const totalInProgress = parseInt(dist.total_in_progress) || 0;

      const completionRate = totalApplications > 0 ? (totalCompleted / totalApplications) * 100 : 0;
      const overdueRate = totalApplications > 0 ? (totalOverdue / totalApplications) * 100 : 0;

      // Средний рейтинг сотрудников участка
      const avgScoreResult = await pool.query(`
        SELECT AVG(performance_score) as avg_score
        FROM employee_performance_stats
        WHERE district_id = $1 AND period_start = $2 AND period_end >= $3
      `, [districtId, periodStart, periodStart]);
      const averageScore = parseFloat(avgScoreResult.rows[0]?.avg_score) || 0;

      await pool.query(`
        INSERT INTO district_performance_stats
          (district_id, period_start, period_end, total_applications, total_completed,
           total_overdue, total_in_progress, completion_rate, overdue_rate, 
           average_score, calculated_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
        ON CONFLICT (district_id, period_start, period_end)
        DO UPDATE SET
          total_applications = EXCLUDED.total_applications,
          total_completed = EXCLUDED.total_completed,
          total_overdue = EXCLUDED.total_overdue,
          total_in_progress = EXCLUDED.total_in_progress,
          completion_rate = EXCLUDED.completion_rate,
          overdue_rate = EXCLUDED.overdue_rate,
          average_score = EXCLUDED.average_score,
          updated_at = EXCLUDED.updated_at
      `, [
        districtId,
        periodStart,
        periodEnd,
        totalApplications,
        totalCompleted,
        totalOverdue,
        totalInProgress,
        completionRate,
        overdueRate,
        averageScore,
        now,
        now,
      ]);
    }

    await pool.query('COMMIT');
    console.log('[calculatePerformanceStats] Статистика производительности рассчитана успешно');
  } catch (err) {
    await pool.query('ROLLBACK').catch(() => {});
    console.error('[calculatePerformanceStats] Ошибка расчёта статистики:', err.message || err);
    throw err;
  }
}

// Ручной запуск синхронизации (поддерживаем и POST, и GET для удобства)
const handleDomaSyncNow = async (req, res) => {
  try {
    const result = await syncDomaApplications();
    res.json({ success: true, ...result });
  } catch (err) {
    res.status(500).json({
      success: false,
      error: err.message || 'Failed to sync Doma AI applications',
    });
  }
};

app.post(`${API_PREFIX}/doma/sync-now`, handleDomaSyncNow);
app.get(`${API_PREFIX}/doma/sync-now`, handleDomaSyncNow);

// API для обновления заявки в Doma.AI и локальной БД
app.put(`${API_PREFIX}/applications/:number/status`, async (req, res) => {
  try {
    const { number } = req.params;
    const { status, comment, deferredUntil, domaApiUrl, domaToken } = req.body;

    if (!status) {
      return res.status(400).json({ success: false, error: 'Статус обязателен' });
    }

    // Находим заявку в локальной БД по номеру
    const appResult = await pool.query(
      `SELECT id, number, status FROM applications WHERE number = $1 LIMIT 1`,
      [number]
    );

    if (appResult.rows.length === 0) {
      return res.status(404).json({ success: false, error: 'Заявка не найдена' });
    }

    const localApp = appResult.rows[0];

    // Маппинг локальных статусов в статусы Doma.AI
    const statusTypeMap = {
      new: 'new_or_reopened',
      in_progress: 'processing',
      deferred: 'deferred',
      done: 'completed',
      canceled: 'canceled',
    };

    const domaStatusType = statusTypeMap[status] || status;

    // Получаем настройки Doma.AI из запроса, БД (панель интеграций) или env
    let apiUrl = domaApiUrl || null;
    let token = domaToken || null;
    if (!apiUrl || !token) {
      const fromDb = await getDomaSettingsFromDb();
      apiUrl = apiUrl || fromDb.apiUrl;
      token = token || fromDb.token;
    }

    if (!apiUrl || !token) {
      return res.status(400).json({ success: false, error: 'Настройки Doma.AI не настроены' });
    }

    // Сначала находим заявку в Doma.AI по номеру, чтобы получить её ID и текущий статус
    // Пробуем искать как по строке, так и по числу
    const ticketNumber = isNaN(Number(number)) ? number : Number(number);
    
    // Используем allTickets вместо tickets (согласно ошибке GraphQL)
    const findTicketQuery = `
      query FindTicket($where: TicketWhereInput, $first: Int) {
        objs: allTickets(where: $where, first: $first) {
          id
          number
          status {
            id
            type
            name
          }
        }
      }
    `;

    let findResponse;
    try {
      findResponse = await axios.post(
        apiUrl,
        {
          query: findTicketQuery,
          variables: {
            where: {
              number: ticketNumber,
            },
            first: 1,
          },
        },
        {
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${token}`,
          },
        }
      );
    } catch (findErr) {
      console.error('[updateApplicationStatus] Ошибка при поиске заявки в Doma.AI:', findErr.response?.data || findErr.message);
      return res.status(400).json({
        success: false,
        error: `Ошибка при поиске заявки в Doma.AI: ${findErr.response?.data?.errors?.[0]?.message || findErr.message}`,
      });
    }

    if (findResponse.data.errors) {
      console.error('[updateApplicationStatus] Ошибки GraphQL при поиске:', findResponse.data.errors);
      return res.status(400).json({
        success: false,
        error: `Ошибка GraphQL: ${findResponse.data.errors[0]?.message || 'Неизвестная ошибка'}`,
      });
    }

    if (!findResponse.data.data?.objs?.[0]) {
      console.warn(`[updateApplicationStatus] Заявка с номером ${number} не найдена в Doma.AI`);
      return res.status(404).json({
        success: false,
        error: 'Заявка не найдена в Doma.AI',
      });
    }

    const domaTicket = findResponse.data.data.objs[0];
    const domaTicketId = domaTicket.id;
    const currentStatus = domaTicket.status;
    console.log(`[updateApplicationStatus] Найдена заявка в Doma.AI: ID=${domaTicketId}, номер=${number}, текущий статус:`, currentStatus);

    // Обновляем заявку в Doma.AI через GraphQL
    // Формируем данные для обновления
    const updateData = {};
    
    // Статус - нужно найти ID статуса по типу, так как TicketStatusRelateToOneInput принимает только id
    let statusId = null;
    
    // Если текущий статус имеет нужный тип, используем его ID
    if (currentStatus && currentStatus.type === domaStatusType && currentStatus.id) {
      statusId = currentStatus.id;
      console.log(`[updateApplicationStatus] Используем ID текущего статуса: ${statusId}`);
    } else {
      // Ищем статус по типу через запрос
      const statusQuery = `
        query GetStatusByType($where: TicketStatusWhereInput) {
          objs: allTicketStatuses(where: $where, first: 1) {
            id
            type
            name
          }
        }
      `;
      
      try {
        const statusResponse = await axios.post(
          apiUrl,
          {
            query: statusQuery,
            variables: {
              where: {
                type: domaStatusType,
              },
            },
          },
          {
            headers: {
              'Content-Type': 'application/json',
              Authorization: `Bearer ${token}`,
            },
          }
        );
        
        if (statusResponse.data.data?.objs?.[0]?.id) {
          statusId = statusResponse.data.data.objs[0].id;
          console.log(`[updateApplicationStatus] Найден ID статуса по типу ${domaStatusType}: ${statusId}`);
        } else {
          throw new Error(`Статус с типом "${domaStatusType}" не найден в Doma.AI`);
        }
      } catch (statusErr) {
        console.error('[updateApplicationStatus] Ошибка при поиске статуса:', statusErr.response?.data || statusErr.message);
        return res.status(400).json({
          success: false,
          error: `Не удалось найти статус "${domaStatusType}" в Doma.AI: ${statusErr.response?.data?.errors?.[0]?.message || statusErr.message}`,
        });
      }
    }
    
    // Используем connect для связи со статусом (формат Keystone.js)
    updateData.status = { connect: { id: statusId } };
    
    // Комментарий (причина смены статуса)
    if (comment && comment.trim()) {
      updateData.statusReason = comment.trim();
      updateData.lastCommentWithResidentTypeAt = new Date().toISOString();
    }
    
    // Дата отложения (только для статуса deferred)
    if (deferredUntil && status === 'deferred') {
      const deferredDate = new Date(deferredUntil);
      if (!isNaN(deferredDate.getTime())) {
        updateData.deferredUntil = deferredDate.toISOString();
      }
    }
    
    // Дата обновления статуса
    updateData.statusUpdatedAt = new Date().toISOString();

    const updateMutation = `
      mutation UpdateTicket($id: ID!, $data: TicketUpdateInput!) {
        obj: updateTicket(id: $id, data: $data) {
          id
          number
          status {
            id
            name
            type
          }
          updatedAt
        }
      }
    `;

    console.log(`[updateApplicationStatus] Обновление заявки ${domaTicketId} с данными:`, JSON.stringify(updateData, null, 2));

    let updateResponse;
    try {
      updateResponse = await axios.post(
        apiUrl,
        {
          query: updateMutation,
          variables: {
            id: domaTicketId,
            data: updateData,
          },
        },
        {
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${token}`,
          },
        }
      );
    } catch (updateErr) {
      console.error('[updateApplicationStatus] Ошибка при обновлении заявки в Doma.AI:', updateErr.response?.data || updateErr.message);
      return res.status(400).json({
        success: false,
        error: `Ошибка при обновлении заявки: ${updateErr.response?.data?.errors?.[0]?.message || updateErr.message}`,
        details: updateErr.response?.data,
      });
    }

    if (updateResponse.data.errors) {
      console.error('[updateApplicationStatus] Ошибки GraphQL при обновлении:', updateResponse.data.errors);
      return res.status(400).json({
        success: false,
        error: updateResponse.data.errors[0]?.message || 'Ошибка при обновлении заявки в Doma.AI',
        details: updateResponse.data.errors,
      });
    }
    
    console.log(`[updateApplicationStatus] Заявка успешно обновлена в Doma.AI`);

    // Обновляем заявку в локальной БД
    const now = new Date();
    await pool.query(
      `UPDATE applications 
       SET status = $1, updated_at = $2 
       WHERE number = $3`,
      [status, now.toISOString(), number]
    );

    res.json({
      success: true,
      data: {
        id: localApp.id,
        number: localApp.number,
        status: status,
      },
    });
  } catch (err) {
    console.error('[updateApplicationStatus] Ошибка:', err);
    res.status(500).json({
      success: false,
      error: err.message || 'Ошибка при обновлении статуса заявки',
    });
  }
});

// ====== ЖУРНАЛ ОТКЛЮЧЕНИЙ (OUTAGES) ======

// GET /api/outages -> список отключений (?buildingId=, ?active=true)
app.get(`${API_PREFIX}/outages`, async (req, res) => {
  try {
    const { buildingId, active } = req.query;
    let sql = `
      SELECT o.id, o.building_id AS "buildingId", o.start_at AS "startAt", o.end_at AS "endAt",
             o.type, o.description, o.active, o.created_at AS "createdAt", o.updated_at AS "updatedAt",
             o.author_name AS "authorName", o.category, o.problem_detail AS "problemDetail",
             o.work_type AS "workType", o.resident_message AS "residentMessage", o.generate_news AS "generateNews",
             b.data->'passport'->>'address' AS "buildingAddress"
      FROM outages o
      LEFT JOIN buildings b ON b.id = o.building_id
      WHERE 1=1
    `;
    const params = [];
    let idx = 1;
    if (buildingId) {
      sql += ` AND o.building_id = $${idx}`;
      params.push(buildingId);
      idx++;
    }
    if (active === 'true' || active === 'false') {
      sql += ` AND o.active = $${idx}`;
      params.push(active === 'true');
      idx++;
    }
    sql += ` ORDER BY o.start_at DESC`;
    const { rows } = await pool.query(sql, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching outages:', err);
    res.status(500).json({ error: err.message || 'Failed to fetch outages' });
  }
});

// POST /api/outages -> создание отключения (один дом или несколько: buildingId или buildingIds)
app.post(`${API_PREFIX}/outages`, async (req, res) => {
  try {
    const { buildingId, buildingIds, startAt, endAt, type, description, active, authorName, category, problemDetail, workType, residentMessage, generateNews } = req.body || {};
    const ids = buildingIds && Array.isArray(buildingIds) && buildingIds.length > 0 ? buildingIds : (buildingId ? [buildingId] : []);
    if (ids.length === 0) {
      return res.status(400).json({ error: 'Укажите buildingId или buildingIds' });
    }
    const start = startAt ? new Date(startAt) : new Date();
    const created = [];
    for (const bid of ids) {
      const { rows } = await pool.query(
        `INSERT INTO outages (building_id, start_at, end_at, type, description, active, updated_at, author_name, category, problem_detail, work_type, resident_message, generate_news)
         VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9, $10, $11, $12)
         RETURNING id, building_id AS "buildingId", start_at AS "startAt", end_at AS "endAt", type, description, active, created_at AS "createdAt", updated_at AS "updatedAt", author_name AS "authorName", category, problem_detail AS "problemDetail", work_type AS "workType", resident_message AS "residentMessage", generate_news AS "generateNews"`,
        [bid, start, endAt ? new Date(endAt) : null, type || null, description || null, active !== false, authorName || null, category || null, problemDetail || null, workType || null, residentMessage || null, !!generateNews]
      );
      created.push(rows[0]);
    }
    try {
      const firstId = created[0]?.id;
      await notificationService.createNotificationForResponsibleZone(pool, 'requests', 'outages', {
        type: 'outage',
        title: 'Новое отключение',
        body: description ? String(description).slice(0, 200) : 'Создано отключение в реестре',
        entityType: 'outage',
        entityId: firstId != null ? String(firstId) : null,
      });
    } catch (notifErr) {
      console.warn('Notification (outage create):', notifErr.message);
    }
    res.status(201).json(created.length === 1 ? created[0] : { created: created.length, items: created });
  } catch (err) {
    console.error('Error creating outage:', err);
    res.status(500).json({ error: err.message || 'Failed to create outage' });
  }
});

// GET /api/outages/:id -> одно отключение (для карточки)
app.get(`${API_PREFIX}/outages/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { rows } = await pool.query(
      `SELECT o.id, o.building_id AS "buildingId", o.start_at AS "startAt", o.end_at AS "endAt",
              o.type, o.description, o.active, o.created_at AS "createdAt", o.updated_at AS "updatedAt",
              o.author_name AS "authorName", o.category, o.problem_detail AS "problemDetail",
              o.work_type AS "workType", o.resident_message AS "residentMessage", o.generate_news AS "generateNews",
              b.data->'passport'->>'address' AS "buildingAddress"
       FROM outages o
       LEFT JOIN buildings b ON b.id = o.building_id
       WHERE o.id = $1`,
      [id]
    );
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Отключение не найдено' });
    }
    res.json(rows[0]);
  } catch (err) {
    console.error('Error fetching outage:', err);
    res.status(500).json({ error: err.message || 'Failed to fetch outage' });
  }
});

// PATCH /api/outages/:id -> обновление отключения (завершить и т.д.)
app.patch(`${API_PREFIX}/outages/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { endAt, type, description, active, category, problemDetail, workType, residentMessage, generateNews } = req.body || {};
    const updates = [];
    const values = [];
    let idx = 1;
    if (endAt !== undefined) {
      updates.push(`end_at = $${idx}`);
      values.push(new Date(endAt));
      idx++;
    }
    if (type !== undefined) {
      updates.push(`type = $${idx}`);
      values.push(type);
      idx++;
    }
    if (description !== undefined) {
      updates.push(`description = $${idx}`);
      values.push(description);
      idx++;
    }
    if (active !== undefined) {
      updates.push(`active = $${idx}`);
      values.push(!!active);
      idx++;
    }
    if (category !== undefined) {
      updates.push(`category = $${idx}`);
      values.push(category);
      idx++;
    }
    if (problemDetail !== undefined) {
      updates.push(`problem_detail = $${idx}`);
      values.push(problemDetail);
      idx++;
    }
    if (workType !== undefined) {
      updates.push(`work_type = $${idx}`);
      values.push(workType);
      idx++;
    }
    if (residentMessage !== undefined) {
      updates.push(`resident_message = $${idx}`);
      values.push(residentMessage);
      idx++;
    }
    if (generateNews !== undefined) {
      updates.push(`generate_news = $${idx}`);
      values.push(!!generateNews);
      idx++;
    }
    if (updates.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }
    updates.push('updated_at = NOW()');
    values.push(id);
    const sql = `UPDATE outages SET ${updates.join(', ')} WHERE id = $${idx} RETURNING id, building_id AS "buildingId", start_at AS "startAt", end_at AS "endAt", type, description, active, created_at AS "createdAt", updated_at AS "updatedAt", author_name AS "authorName", category, problem_detail AS "problemDetail", work_type AS "workType", resident_message AS "residentMessage", generate_news AS "generateNews"`;
    const { rows } = await pool.query(sql, values);
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Отключение не найдено' });
    }
    try {
      await notificationService.createNotificationForResponsibleZone(pool, 'requests', 'outages', {
        type: 'outage',
        title: 'Изменение отключения',
        body: 'Обновлено отключение в реестре',
        entityType: 'outage',
        entityId: String(id),
      });
    } catch (notifErr) {
      console.warn('Notification (outage update):', notifErr.message);
    }
    res.json(rows[0]);
  } catch (err) {
    console.error('Error updating outage:', err);
    res.status(500).json({ error: err.message || 'Failed to update outage' });
  }
});

// ====== API ДЛЯ УПРАВЛЕНИЯ СОПОСТАВЛЕНИЯМИ DOMA.AI ======

// GET /api/doma/mappings -> получить все сопоставления
app.get(`${API_PREFIX}/doma/mappings`, async (req, res) => {
  try {
    const [addressMappings, employeeMappings] = await Promise.all([
      pool.query(`
        SELECT 
          dam.id,
          dam.doma_address,
          dam.building_id,
          b.data->'passport'->>'address' as building_address,
          dam.created_at
        FROM doma_address_mappings dam
        LEFT JOIN buildings b ON b.id = dam.building_id
        ORDER BY dam.created_at DESC
      `),
      pool.query(`
        SELECT 
          dem.id,
          dem.doma_name,
          dem.employee_id,
          e.name as employee_name,
          dem.created_at
        FROM doma_employee_mappings dem
        LEFT JOIN employees e ON e.id = dem.employee_id
        ORDER BY dem.created_at DESC
      `),
    ]);

    res.json({
      success: true,
      data: {
        addresses: addressMappings.rows.map(r => ({
          id: r.id,
          domaAddress: r.doma_address,
          buildingId: r.building_id,
          buildingAddress: r.building_address,
          createdAt: r.created_at,
        })),
        employees: employeeMappings.rows.map(r => ({
          id: r.id,
          domaName: r.doma_name,
          employeeId: r.employee_id,
          employeeName: r.employee_name,
          createdAt: r.created_at,
        })),
      },
    });
  } catch (err) {
    console.error('[getDomaMappings] Ошибка:', err);
    res.status(500).json({ success: false, error: err.message || 'Ошибка при получении сопоставлений' });
  }
});

// GET /api/doma/pending-mappings -> получить ожидающие сопоставления (неразрешённые)
app.get(`${API_PREFIX}/doma/pending-mappings`, async (req, res) => {
  try {
    const result = await pool.query(
      `SELECT id, type, doma_value, suggested_id, suggested_name, created_at
       FROM pending_doma_mappings
       WHERE resolved_at IS NULL
       ORDER BY created_at ASC`
    );

    const rows = result.rows || [];

    res.json({
      success: true,
      data: {
        buildings: rows
          .filter(r => r.type === 'building')
          .map(r => ({
            id: r.id,
            domaValue: r.doma_value,
            suggestedId: r.suggested_id,
            suggestedName: r.suggested_name,
            createdAt: r.created_at,
          })),
        employees: rows
          .filter(r => r.type === 'employee')
          .map(r => ({
            id: r.id,
            domaValue: r.doma_value,
            suggestedId: r.suggested_id,
            suggestedName: r.suggested_name,
            createdAt: r.created_at,
          })),
      },
    });
  } catch (err) {
    console.error('[getPendingDomaMappings] Ошибка:', err);
    res
      .status(500)
      .json({ success: false, error: err.message || 'Ошибка при получении ожидающих сопоставлений' });
  }
});

// POST /api/doma/pending-mappings/:id/resolve -> вручную сопоставить и зафиксировать
app.post(`${API_PREFIX}/doma/pending-mappings/:id/resolve`, async (req, res) => {
  const client = await pool.connect();
  try {
    const { id } = req.params;
    const { targetId } = req.body || {};

    if (!targetId) {
      return res.status(400).json({ success: false, error: 'targetId обязателен' });
    }

    await client.query('BEGIN');

    const pendingResult = await client.query(
      `SELECT * FROM pending_doma_mappings WHERE id = $1 FOR UPDATE`,
      [id]
    );

    if (pendingResult.rows.length === 0) {
      await client.query('ROLLBACK');
      return res.status(404).json({ success: false, error: 'Запись не найдена' });
    }

    const pending = pendingResult.rows[0];

    if (pending.type === 'employee') {
      await client.query(
        `INSERT INTO doma_employee_mappings (doma_name, employee_id)
         VALUES ($1, $2)
         ON CONFLICT (doma_name) DO UPDATE SET employee_id = EXCLUDED.employee_id, updated_at = NOW()`,
        [pending.doma_value, targetId]
      );
    } else if (pending.type === 'building') {
      await client.query(
        `INSERT INTO doma_address_mappings (doma_address, building_id)
         VALUES ($1, $2)
         ON CONFLICT (doma_address) DO UPDATE SET building_id = EXCLUDED.building_id, updated_at = NOW()`,
        [pending.doma_value, targetId]
      );
    }

    await client.query(
      `UPDATE pending_doma_mappings
       SET resolved_at = NOW(),
           resolved_by = $2,
           resolution = 'approved',
           resolved_id = $3
       WHERE id = $1`,
      [id, 'manual', targetId]
    );

    await client.query('COMMIT');

    res.json({ success: true });
  } catch (err) {
    await pool.query('ROLLBACK').catch(() => {});
    console.error('[resolvePendingDomaMapping] Ошибка:', err);
    res
      .status(500)
      .json({ success: false, error: err.message || 'Ошибка при разрешении сопоставления' });
  } finally {
    client.release();
  }
}
);

// PUT /api/doma/mappings/address/:id -> обновить сопоставление адреса
app.put(`${API_PREFIX}/doma/mappings/address/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { buildingId } = req.body;

    if (!buildingId) {
      return res.status(400).json({ success: false, error: 'buildingId обязателен' });
    }

    await pool.query(
      `UPDATE doma_address_mappings 
       SET building_id = $1, updated_at = NOW() 
       WHERE id = $2`,
      [buildingId, id]
    );

    res.json({ success: true });
  } catch (err) {
    console.error('[updateAddressMapping] Ошибка:', err);
    res.status(500).json({ success: false, error: err.message || 'Ошибка при обновлении сопоставления' });
  }
});

// PUT /api/doma/mappings/employee/:id -> обновить сопоставление сотрудника
app.put(`${API_PREFIX}/doma/mappings/employee/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { employeeId } = req.body;

    if (!employeeId) {
      return res.status(400).json({ success: false, error: 'employeeId обязателен' });
    }

    await pool.query(
      `UPDATE doma_employee_mappings 
       SET employee_id = $1, updated_at = NOW() 
       WHERE id = $2`,
      [employeeId, id]
    );

    res.json({ success: true });
  } catch (err) {
    console.error('[updateEmployeeMapping] Ошибка:', err);
    res.status(500).json({ success: false, error: err.message || 'Ошибка при обновлении сопоставления' });
  }
});

// DELETE /api/doma/mappings/address/:id -> удалить сопоставление адреса
app.delete(`${API_PREFIX}/doma/mappings/address/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    await pool.query('DELETE FROM doma_address_mappings WHERE id = $1', [id]);
    res.json({ success: true });
  } catch (err) {
    console.error('[deleteAddressMapping] Ошибка:', err);
    res.status(500).json({ success: false, error: err.message || 'Ошибка при удалении сопоставления' });
  }
});

// DELETE /api/doma/mappings/employee/:id -> удалить сопоставление сотрудника
app.delete(`${API_PREFIX}/doma/mappings/employee/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    await pool.query('DELETE FROM doma_employee_mappings WHERE id = $1', [id]);
    res.json({ success: true });
  } catch (err) {
    console.error('[deleteEmployeeMapping] Ошибка:', err);
    res.status(500).json({ success: false, error: err.message || 'Ошибка при удалении сопоставления' });
  }
});

// API для получения статистики производительности
app.get(`${API_PREFIX}/performance/overall`, async (req, res) => {
  try {
    const now = new Date();
    const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
    const periodEnd = now;

    const stats = await pool.query(`
      SELECT 
        COUNT(*) as total,
        COUNT(*) FILTER (WHERE status = 'done') as completed,
        COUNT(*) FILTER (WHERE is_overdue = true) as overdue,
        COUNT(*) FILTER (WHERE status IN ('in_progress', 'deferred')) as in_progress
      FROM applications
      WHERE created_at >= $1 AND created_at <= $2
    `, [periodStart, periodEnd]);

    const result = stats.rows[0];
    const total = parseInt(result.total) || 0;
    const completed = parseInt(result.completed) || 0;
    const overdue = parseInt(result.overdue) || 0;
    const inProgress = parseInt(result.in_progress) || 0;

    res.json({
      success: true,
      data: {
        total,
        completed,
        overdue,
        inProgress,
        completionRate: total > 0 ? (completed / total) * 100 : 0,
        overdueRate: total > 0 ? (overdue / total) * 100 : 0,
        periodStart,
        periodEnd,
      },
    });
  } catch (err) {
    res.status(500).json({ success: false, error: err.message || 'Failed to get overall performance stats' });
  }
});

app.get(`${API_PREFIX}/performance/employees`, async (req, res) => {
  try {
    const now = new Date();
    const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
    const periodEnd = now;

    const stats = await pool.query(`
      SELECT 
        eps.*,
        d.name as district_name
      FROM employee_performance_stats eps
      LEFT JOIN districts d ON d.id = eps.district_id
      WHERE eps.period_start = $1 AND eps.period_end >= $2
      ORDER BY eps.performance_score DESC
    `, [periodStart, periodStart]);

    res.json({
      success: true,
      data: stats.rows.map((row) => ({
        employeeName: row.employee_name,
        districtId: row.district_id,
        districtName: row.district_name,
        totalAssigned: parseInt(row.total_assigned) || 0,
        totalCompleted: parseInt(row.total_completed) || 0,
        totalOverdue: parseInt(row.total_overdue) || 0,
        totalInProgress: parseInt(row.total_in_progress) || 0,
        totalDeferred: parseInt(row.total_deferred) || 0,
        completionRate: parseFloat(row.completion_rate) || 0,
        overdueRate: parseFloat(row.overdue_rate) || 0,
        performanceScore: parseFloat(row.performance_score) || 0,
      })),
    });
  } catch (err) {
    res.status(500).json({ success: false, error: err.message || 'Failed to get employee performance stats' });
  }
});

app.get(`${API_PREFIX}/performance/districts`, async (req, res) => {
  try {
    const now = new Date();
    const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
    const periodEnd = now;

    const stats = await pool.query(`
      SELECT 
        dps.*,
        d.name as district_name,
        d.manager_name
      FROM district_performance_stats dps
      JOIN districts d ON d.id = dps.district_id
      WHERE dps.period_start = $1 AND dps.period_end >= $2
      ORDER BY dps.completion_rate DESC
    `, [periodStart, periodStart]);

    res.json({
      success: true,
      data: stats.rows.map((row) => ({
        districtId: row.district_id,
        districtName: row.district_name,
        managerName: row.manager_name,
        totalApplications: parseInt(row.total_applications) || 0,
        totalCompleted: parseInt(row.total_completed) || 0,
        totalOverdue: parseInt(row.total_overdue) || 0,
        totalInProgress: parseInt(row.total_in_progress) || 0,
        completionRate: parseFloat(row.completion_rate) || 0,
        overdueRate: parseFloat(row.overdue_rate) || 0,
        averageScore: parseFloat(row.average_score) || 0,
      })),
    });
  } catch (err) {
    res.status(500).json({ success: false, error: err.message || 'Failed to get district performance stats' });
  }
});

// API для получения метрик производства (состояние домов, график работ, SLA)
app.get(`${API_PREFIX}/production/metrics`, async (req, res) => {
  try {
    // 1. Среднее состояние домов (износ)
    // Пробуем разные пути к данным износа
    const buildingsCondition = await pool.query(`
      SELECT 
        AVG(
          COALESCE(
            (data->'passport'->'construction'->>'wearPercent')::numeric,
            (data->'passport'->'general'->>'wearPercent')::numeric,
            NULL
          )
        ) as avg_wear_percent,
        COUNT(*) FILTER (WHERE 
          data->'passport'->'construction'->>'wearPercent' IS NOT NULL OR
          data->'passport'->'general'->>'wearPercent' IS NOT NULL
        ) as buildings_with_wear,
        COUNT(*) as total_buildings
      FROM buildings
    `);

    const avgWearPercent = parseFloat(buildingsCondition.rows[0]?.avg_wear_percent) || 0;
    const buildingsWithWear = parseInt(buildingsCondition.rows[0]?.buildings_with_wear) || 0;
    const totalBuildings = parseInt(buildingsCondition.rows[0]?.total_buildings) || 0;

    // Оценка состояния на основе износа
    let buildingCondition = 'excellent'; // excellent, good, fair, poor
    let buildingConditionLabel = 'Отличное';
    if (avgWearPercent >= 70) {
      buildingCondition = 'poor';
      buildingConditionLabel = 'Плохое';
    } else if (avgWearPercent >= 50) {
      buildingCondition = 'fair';
      buildingConditionLabel = 'Удовлетворительное';
    } else if (avgWearPercent >= 30) {
      buildingCondition = 'good';
      buildingConditionLabel = 'Хорошее';
    }

    // 2. График работ (соблюдение и опоздание)
    // Считаем выполненные заявки: в срок (deadline >= дата выполнения) и с опозданием
    const workSchedule = await pool.query(`
      SELECT 
        COUNT(*) FILTER (WHERE status = 'done') as total_done,
        COUNT(*) FILTER (WHERE status = 'done' AND deadline_at >= updated_at) as on_time,
        COUNT(*) FILTER (WHERE status = 'done' AND deadline_at < updated_at) as late,
        COUNT(*) FILTER (WHERE is_overdue = true AND status != 'done') as overdue_active
      FROM applications
      WHERE status = 'done' OR (is_overdue = true AND status != 'done')
    `);

    const totalDone = parseInt(workSchedule.rows[0]?.total_done) || 0;
    const onTime = parseInt(workSchedule.rows[0]?.on_time) || 0;
    const late = parseInt(workSchedule.rows[0]?.late) || 0;
    const overdueActive = parseInt(workSchedule.rows[0]?.overdue_active) || 0;

    const onTimeRate = totalDone > 0 ? (onTime / totalDone) * 100 : 0;
    const lateRate = totalDone > 0 ? (late / totalDone) * 100 : 0;

    // 3. Рейтинг SLA (процент заявок, выполненных в срок от всех завершённых)
    const slaRating = await pool.query(`
      SELECT 
        COUNT(*) FILTER (WHERE status = 'done') as total_done,
        COUNT(*) FILTER (WHERE status = 'done' AND deadline_at >= updated_at) as on_time
      FROM applications
      WHERE status = 'done'
    `);

    const slaTotal = parseInt(slaRating.rows[0]?.total_done) || 0;
    const slaOnTime = parseInt(slaRating.rows[0]?.on_time) || 0;
    const slaRatingPercent = slaTotal > 0 ? (slaOnTime / slaTotal) * 100 : 0;

    res.json({
      success: true,
      data: {
        buildingCondition: {
          value: avgWearPercent,
          label: buildingConditionLabel,
          condition: buildingCondition,
          totalBuildings,
          buildingsWithWear,
        },
        workSchedule: {
          onTime: onTime,
          late: late,
          overdue: overdueActive,
          onTimeRate: Math.round(onTimeRate),
          lateRate: Math.round(lateRate),
        },
        slaRating: Math.round(slaRatingPercent),
      },
    });
  } catch (err) {
    console.error('[production/metrics] Ошибка:', err);
    res.status(500).json({ success: false, error: err.message || 'Failed to get production metrics' });
  }
});

// GET /api/production/dashboard — полный дашборд для продвинутой статистики производства
app.get(`${API_PREFIX}/production/dashboard`, async (req, res) => {
  try {
    const now = new Date();
    const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const weekStart = new Date(todayStart);
    weekStart.setDate(weekStart.getDate() - 7);
    const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);

    const toDateStr = (d) => d.toISOString ? d.toISOString().slice(0, 10) : '';

    // 1. Метрики как в production/metrics (состояние домов, график работ, SLA)
    const buildingsCondition = await pool.query(`
      SELECT 
        AVG(COALESCE((data->'passport'->'construction'->>'wearPercent')::numeric, (data->'passport'->'general'->>'wearPercent')::numeric, NULL)) as avg_wear_percent,
        COUNT(*) FILTER (WHERE data->'passport'->'construction'->>'wearPercent' IS NOT NULL OR data->'passport'->'general'->>'wearPercent' IS NOT NULL) as buildings_with_wear,
        COUNT(*) as total_buildings
      FROM buildings
    `);
    const r0 = buildingsCondition.rows[0];
    const avgWearPercent = parseFloat(r0?.avg_wear_percent) || 0;
    const buildingsWithWear = parseInt(r0?.buildings_with_wear) || 0;
    const totalBuildings = parseInt(r0?.total_buildings) || 0;
    let buildingCondition = 'excellent';
    let buildingConditionLabel = 'Отличное';
    if (avgWearPercent >= 70) { buildingCondition = 'poor'; buildingConditionLabel = 'Плохое'; }
    else if (avgWearPercent >= 50) { buildingCondition = 'fair'; buildingConditionLabel = 'Удовлетворительное'; }
    else if (avgWearPercent >= 30) { buildingCondition = 'good'; buildingConditionLabel = 'Хорошее'; }

    const workSchedule = await pool.query(`
      SELECT 
        COUNT(*) FILTER (WHERE status = 'done') as total_done,
        COUNT(*) FILTER (WHERE status = 'done' AND deadline_at >= updated_at) as on_time,
        COUNT(*) FILTER (WHERE status = 'done' AND deadline_at < updated_at) as late,
        COUNT(*) FILTER (WHERE is_overdue = true AND status != 'done') as overdue_active
      FROM applications
      WHERE status = 'done' OR (is_overdue = true AND status != 'done')
    `);
    const ws = workSchedule.rows[0];
    const totalDone = parseInt(ws?.total_done) || 0;
    const onTime = parseInt(ws?.on_time) || 0;
    const late = parseInt(ws?.late) || 0;
    const overdueActive = parseInt(ws?.overdue_active) || 0;
    const onTimeRate = totalDone > 0 ? (onTime / totalDone) * 100 : 0;
    const lateRate = totalDone > 0 ? (late / totalDone) * 100 : 0;

    const slaRatingQ = await pool.query(`
      SELECT COUNT(*) FILTER (WHERE status = 'done') as total_done, COUNT(*) FILTER (WHERE status = 'done' AND deadline_at >= updated_at) as on_time
      FROM applications WHERE status = 'done'
    `);
    const slaR = slaRatingQ.rows[0];
    const slaTotal = parseInt(slaR?.total_done) || 0;
    const slaOnTime = parseInt(slaR?.on_time) || 0;
    const slaRatingPercent = slaTotal > 0 ? (slaOnTime / slaTotal) * 100 : 0;

    const buildingConditionData = {
      value: avgWearPercent,
      label: buildingConditionLabel,
      condition: buildingCondition,
      totalBuildings,
      buildingsWithWear,
    };
    const workScheduleData = {
      onTime,
      late,
      overdue: overdueActive,
      onTimeRate: Math.round(onTimeRate),
      lateRate: Math.round(lateRate),
    };
    const slaRating = Math.round(slaRatingPercent);

    // 2. Заявки — временные срезы (сегодня, неделя, месяц): создано, закрыто, просрочено
    const hasUpdatedAt = await pool.query(`
      SELECT column_name FROM information_schema.columns WHERE table_name = 'applications' AND column_name = 'updated_at'
    `);
    const useUpdatedAt = hasUpdatedAt.rows.length > 0;

    const todayStr = toDateStr(todayStart);
    const appsToday = await pool.query(`
      SELECT 
        COUNT(*) FILTER (WHERE (created_at::date)::text = $1) as created,
        COUNT(*) FILTER (WHERE status = 'done' AND ${useUpdatedAt ? "(updated_at::date)::text = $1" : "(created_at::date)::text = $1"}) as closed,
        COUNT(*) FILTER (WHERE is_overdue = true AND status != 'done' AND status != 'canceled') as overdue_now
      FROM applications
    `, [todayStr]);
    const appsWeek = await pool.query(`
      SELECT 
        COUNT(*) FILTER (WHERE created_at >= $1) as created,
        COUNT(*) FILTER (WHERE status = 'done' AND ${useUpdatedAt ? "updated_at >= $1" : "created_at >= $1"}) as closed,
        COUNT(*) FILTER (WHERE is_overdue = true AND status != 'done' AND status != 'canceled') as overdue_now
      FROM applications
      WHERE created_at >= $1 OR status = 'done' OR (is_overdue = true AND status != 'done')
    `, [weekStart]);
    const appsMonth = await pool.query(`
      SELECT 
        COUNT(*) FILTER (WHERE created_at >= $1) as created,
        COUNT(*) FILTER (WHERE status = 'done' AND ${useUpdatedAt ? "updated_at >= $1" : "created_at >= $1"}) as closed,
        COUNT(*) FILTER (WHERE is_overdue = true AND status != 'done' AND status != 'canceled') as overdue_now
      FROM applications
      WHERE created_at >= $1 OR status = 'done' OR (is_overdue = true AND status != 'done')
    `, [monthStart]);

    const applicationsTimeSlices = {
      today: {
        created: parseInt(appsToday.rows[0]?.created) || 0,
        closed: parseInt(appsToday.rows[0]?.closed) || 0,
        overdueNow: parseInt(appsToday.rows[0]?.overdue_now) || 0,
      },
      week: {
        created: parseInt(appsWeek.rows[0]?.created) || 0,
        closed: parseInt(appsWeek.rows[0]?.closed) || 0,
        overdueNow: parseInt(appsWeek.rows[0]?.overdue_now) || 0,
      },
      month: {
        created: parseInt(appsMonth.rows[0]?.created) || 0,
        closed: parseInt(appsMonth.rows[0]?.closed) || 0,
        overdueNow: parseInt(appsMonth.rows[0]?.overdue_now) || 0,
      },
    };

    // 3. Сводка по статусам заявок
    const statusSummary = await pool.query(`
      SELECT 
        COUNT(*) FILTER (WHERE status = 'new') as new,
        COUNT(*) FILTER (WHERE status = 'in_progress') as in_progress,
        COUNT(*) FILTER (WHERE status = 'deferred') as deferred,
        COUNT(*) FILTER (WHERE status = 'done') as done,
        COUNT(*) FILTER (WHERE is_overdue = true AND status != 'done' AND status != 'canceled') as overdue
      FROM applications
      WHERE status != 'canceled'
    `);
    const ss = statusSummary.rows[0];
    const applicationsSummary = {
      new: parseInt(ss?.new) || 0,
      inProgress: parseInt(ss?.in_progress) || 0,
      deferred: parseInt(ss?.deferred) || 0,
      done: parseInt(ss?.done) || 0,
      overdue: parseInt(ss?.overdue) || 0,
    };
    const totalActive = applicationsSummary.new + applicationsSummary.inProgress + applicationsSummary.deferred;
    const completionRate = applicationsSummary.done + totalActive > 0
      ? Math.round((applicationsSummary.done / (applicationsSummary.done + totalActive)) * 100)
      : 0;

    // 4. Участки с метриками (performance/districts + кол-во домов)
    const districtsWithBuildings = await pool.query(`
      SELECT data->>'districtId' as district_id, COUNT(*) as building_count
      FROM buildings
      WHERE data->>'districtId' IS NOT NULL AND data->>'districtId' != ''
      GROUP BY data->>'districtId'
    `);
    const buildingCountByDistrict = {};
    districtsWithBuildings.rows.forEach((row) => {
      buildingCountByDistrict[row.district_id] = parseInt(row.building_count) || 0;
    });

    const nowPeriod = new Date(now.getFullYear(), now.getMonth(), 1);
    const districtStats = await pool.query(`
      SELECT dps.*, d.name as district_name, d.manager_name
      FROM district_performance_stats dps
      JOIN districts d ON d.id = dps.district_id
      WHERE dps.period_start = $1 AND dps.period_end >= $2
      ORDER BY dps.completion_rate DESC
    `, [nowPeriod, nowPeriod]);
    const districts = districtStats.rows.map((row) => ({
      districtId: row.district_id,
      districtName: row.district_name,
      managerName: row.manager_name,
      buildingCount: buildingCountByDistrict[row.district_id] ?? 0,
      totalApplications: parseInt(row.total_applications) || 0,
      totalCompleted: parseInt(row.total_completed) || 0,
      totalOverdue: parseInt(row.total_overdue) || 0,
      totalInProgress: parseInt(row.total_in_progress) || 0,
      completionRate: parseFloat(row.completion_rate) || 0,
      overdueRate: parseFloat(row.overdue_rate) || 0,
      averageScore: parseFloat(row.average_score) || 0,
    }));
    // Участки без статистики в district_performance_stats — показываем только дома
    const allDistricts = await pool.query('SELECT id, name, manager_name FROM districts ORDER BY id');
    const districtIdsInStats = new Set(districts.map((d) => d.districtId));
    allDistricts.rows.forEach((row) => {
      if (!districtIdsInStats.has(row.id)) {
        districts.push({
          districtId: row.id,
          districtName: row.name,
          managerName: row.manager_name,
          buildingCount: buildingCountByDistrict[row.id] ?? 0,
          totalApplications: 0,
          totalCompleted: 0,
          totalOverdue: 0,
          totalInProgress: 0,
          completionRate: 0,
          overdueRate: 0,
          averageScore: 0,
        });
      }
    });

    // 5. Топ исполнителей (активные заявки: не done/canceled)
    const performers = await pool.query(`
      SELECT COALESCE(performer_name, 'Не назначен') as name, COUNT(*) as cnt
      FROM applications
      WHERE status NOT IN ('done', 'canceled') AND (performer_name IS NOT NULL AND performer_name != '')
      GROUP BY performer_name
      ORDER BY cnt DESC
      LIMIT 10
    `);
    const topPerformers = performers.rows.map((row) => ({ name: row.name, activeCount: parseInt(row.cnt) || 0 }));

    // 6. Агрегаты из buildings.data: план работ, обходы, списания, обходы счетчиков
    const buildingsData = await pool.query(`
      SELECT id, data->>'districtId' as district_id,
        data->'annualPlan' as annual_plan,
        data->'inspectionHistory' as inspection_history,
        data->'writeOffHistory' as write_off_history,
        data->'meterCheckRounds' as meter_check_rounds
      FROM buildings
    `);

    const currentYear = now.getFullYear();
    const currentMonthNames = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
    const currentMonthName = currentMonthNames[now.getMonth()];

    let planWorksTotal = 0;
    let planWorksCompleted = 0;
    let planWorksCurrent = 0;
    let planWorksCarriedOver = 0;
    let planWorksEstimatedCost = 0;
    const planWorksByDistrict = {};

    let inspectionsCountMonth = 0;
    let inspectionsCountQuarter = 0;
    let lastInspectionDate = null;
    const inspectionsByDistrict = {};
    const quarterStart = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1);

    let writeOffsCountToday = 0;
    let writeOffsCountWeek = 0;
    let writeOffsCountMonth = 0;
    let writeOffsSumMonth = 0;
    const writeOffsByDistrict = {};

    let meterRoundsCountMonth = 0;
    let lastMeterRoundDate = null;
    const meterRoundsByDistrict = {};

    buildingsData.rows.forEach((row) => {
      const districtId = row.district_id || 'unknown';
      if (!planWorksByDistrict[districtId]) planWorksByDistrict[districtId] = { total: 0, completed: 0, buildingCount: 0 };
      if (!inspectionsByDistrict[districtId]) inspectionsByDistrict[districtId] = { count: 0, lastDate: null };
      if (!writeOffsByDistrict[districtId]) writeOffsByDistrict[districtId] = { count: 0, sum: 0 };
      if (!meterRoundsByDistrict[districtId]) meterRoundsByDistrict[districtId] = { count: 0, lastDate: null };

      const plan = row.annual_plan;
      let buildingHadPlanThisMonth = false;
      if (plan && Array.isArray(plan)) {
        plan.forEach((item) => {
          if (item.year !== currentYear) return;
          if (item.month !== currentMonthName) return;
          buildingHadPlanThisMonth = true;
          planWorksTotal++;
          planWorksEstimatedCost += Number(item.estimatedCost) || 0;
          if (item.status === 'completed') { planWorksCompleted++; planWorksByDistrict[districtId].completed++; }
          else if (item.status === 'current' || item.status === 'future') planWorksCurrent++;
          else if (item.status === 'carried_over') planWorksCarriedOver++;
          planWorksByDistrict[districtId].total++;
        });
        if (buildingHadPlanThisMonth) planWorksByDistrict[districtId].buildingCount++;
      }

      const insp = row.inspection_history;
      if (insp && Array.isArray(insp)) {
        insp.forEach((act) => {
          const d = act.date ? act.date.slice(0, 10) : null;
          if (!d) return;
          const actDate = new Date(d);
          if (actDate >= monthStart) inspectionsCountMonth++;
          if (actDate >= quarterStart) inspectionsCountQuarter++;
          if (!lastInspectionDate || d > lastInspectionDate) lastInspectionDate = d;
          inspectionsByDistrict[districtId].count++;
          const distLast = inspectionsByDistrict[districtId].lastDate;
          if (!distLast || d > distLast) inspectionsByDistrict[districtId].lastDate = d;
        });
      }

      const writeOff = row.write_off_history;
      if (writeOff && Array.isArray(writeOff)) {
        writeOff.forEach((act) => {
          const d = act.date ? act.date.slice(0, 10) : null;
          if (!d) return;
          const actDate = new Date(d);
          const amount = Number(act.totalAmount) || 0;
          if (d >= toDateStr(todayStart)) writeOffsCountToday++;
          if (actDate >= weekStart) writeOffsCountWeek++;
          if (actDate >= monthStart) {
            writeOffsCountMonth++;
            writeOffsSumMonth += amount;
            writeOffsByDistrict[districtId].count++;
            writeOffsByDistrict[districtId].sum += amount;
          }
        });
      }

      const rounds = row.meter_check_rounds;
      if (rounds && Array.isArray(rounds)) {
        rounds.forEach((r) => {
          const d = r.date ? r.date.slice(0, 10) : null;
          if (!d) return;
          const roundDate = new Date(d);
          if (roundDate >= monthStart) { meterRoundsCountMonth++; meterRoundsByDistrict[districtId].count++; }
          if (!lastMeterRoundDate || d > lastMeterRoundDate) lastMeterRoundDate = d;
          if (!meterRoundsByDistrict[districtId].lastDate || d > meterRoundsByDistrict[districtId].lastDate) meterRoundsByDistrict[districtId].lastDate = d;
        });
      }
    });

    const planWorks = {
      total: planWorksTotal,
      completed: planWorksCompleted,
      current: planWorksCurrent,
      carriedOver: planWorksCarriedOver,
      estimatedCostMonth: Math.round(planWorksEstimatedCost),
      byDistrict: Object.entries(planWorksByDistrict).map(([did, v]) => ({ districtId: did, ...v })),
    };
    const inspections = {
      countMonth: inspectionsCountMonth,
      countQuarter: inspectionsCountQuarter,
      lastDate: lastInspectionDate,
      byDistrict: Object.entries(inspectionsByDistrict).map(([did, v]) => ({ districtId: did, ...v })),
    };
    const writeOffs = {
      countToday: writeOffsCountToday,
      countWeek: writeOffsCountWeek,
      countMonth: writeOffsCountMonth,
      sumMonth: Math.round(writeOffsSumMonth),
      byDistrict: Object.entries(writeOffsByDistrict).map(([did, v]) => ({ districtId: did, ...v })),
    };
    const meterRounds = {
      countMonth: meterRoundsCountMonth,
      lastDate: lastMeterRoundDate,
      byDistrict: Object.entries(meterRoundsByDistrict).map(([did, v]) => ({ districtId: did, ...v })),
    };

    // Остатки складов участков (districts.inventory — суммарное кол-во позиций)
    const districtsRows = await pool.query('SELECT id, name, manager_name, COALESCE(inventory, \'[]\'::jsonb) AS inventory FROM districts ORDER BY id');
    const districtInventorySummary = districtsRows.rows.map((row) => {
      const inv = row.inventory;
      const items = Array.isArray(inv) ? inv : [];
      const totalItems = items.reduce((sum, it) => sum + (Number(it.quantity) || 0), 0);
      const totalValue = items.reduce((sum, it) => sum + (Number(it.quantity) || 0) * (Number(it.unitPrice) || 0), 0);
      return { districtId: row.id, districtName: row.name, totalQuantity: totalItems, totalValue: Math.round(totalValue) };
    });

    res.json({
      success: true,
      data: {
        buildingCondition: buildingConditionData,
        workSchedule: workScheduleData,
        slaRating,
        applicationsTimeSlices,
        applicationsSummary: { ...applicationsSummary, completionRate },
        districts,
        topPerformers,
        planWorks,
        inspections,
        writeOffs,
        meterRounds,
        districtInventorySummary,
      },
    });
  } catch (err) {
    console.error('[production/dashboard] Ошибка:', err);
    res.status(500).json({ success: false, error: err.message || 'Failed to get production dashboard' });
  }
});

// Автоматическая синхронизация заявок из Doma AI каждые 30 минут + первый запуск при старте
const DOMA_SYNC_INTERVAL_MS = 30 * 60 * 1000;

// Первый запуск после старта сервера (без блокировки старта)
setTimeout(() => {
  syncDomaApplications().catch((err) => {
    console.error('[backend] Ошибка начальной синхронизации Doma AI:', err.message || err);
  });
}, 10 * 1000); // через 10 секунд после старта

// Плановая синхронизация
setInterval(() => {
  syncDomaApplications().catch((err) => {
    console.error('[backend] Ошибка плановой синхронизации Doma AI:', err.message || err);
  });
}, DOMA_SYNC_INTERVAL_MS);

// POST /api/doma/graphql – прокси для Doma AI GraphQL (обход CORS)
app.post(`${API_PREFIX}/doma/graphql`, async (req, res) => {
  const { query, variables, config } = req.body || {};

  let apiUrl = (config && config.apiUrl) || null;
  let token = (config && config.token) || null;
  if (!apiUrl || !token) {
    const fromDb = await getDomaSettingsFromDb();
    apiUrl = apiUrl || fromDb.apiUrl;
    token = token || fromDb.token;
  }

  if (!apiUrl) {
    return res.status(400).json({ error: 'Doma AI apiUrl is not configured' });
  }

  try {
    const headers = {
      'Content-Type': 'application/json',
    };
    if (token) {
      headers['Authorization'] = `Bearer ${token}`;
    }

    const response = await axios.post(
      apiUrl,
      {
        query,
        variables: variables || {},
      },
      { headers }
    );

    // Пробрасываем статус и тело как есть
    res.status(response.status).json(response.data);
  } catch (err) {
    const status = err.response?.status || 500;
    const data = err.response?.data || { message: err.message };

    console.error('[backend] Error proxying Doma AI request:', status, data);

    res.status(status).json({
      error: 'Failed to call Doma AI',
      details: data,
    });
  }
});

// Health check endpoint для проверки статуса БД
app.get(`${API_PREFIX}/health/db`, async (req, res) => {
  try {
    const result = await query('SELECT NOW() as current_time, version() as pg_version');
    res.json({ 
      status: 'ok', 
      database: 'connected',
      timestamp: result[0].current_time,
      pgVersion: result[0].pg_version.split(' ')[1] // Извлекаем версию
    });
  } catch (err) {
    res.status(500).json({ 
      status: 'error', 
      database: 'disconnected',
      error: err.message 
    });
  }
});

// ========= ФИНАНСОВЫЕ ENDPOINTS =========

// POST /api/finance/upload-report - загрузка файла
app.post(`${API_PREFIX}/finance/upload-report`, upload.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'Файл не загружен' });
    }

    // Правильно декодируем имя файла (исправляем кодировку кириллицы)
    let filename = req.file.originalname;
    try {
      // Multer может передавать имена файлов в неправильной кодировке
      // Пробуем декодировать из latin1 в utf8, если имя содержит кракозябры
      const hasCyrillic = /[А-Яа-яЁё]/.test(filename);
      const hasGarbled = /[ÐÑ]/i.test(filename) || /[â€]/i.test(filename);
      
      if (hasGarbled || (!hasCyrillic && /[^\x00-\x7F]/.test(filename))) {
        // Пробуем перекодировать
        try {
          const buffer = Buffer.from(filename, 'latin1');
          const decoded = buffer.toString('utf8');
          if (/[А-Яа-яЁё]/.test(decoded)) {
            filename = decoded;
            console.log('Исправлена кодировка имени файла:', filename);
          }
        } catch (decodeErr) {
          // Если не получилось, пробуем другой способ
          try {
            filename = decodeURIComponent(escape(filename));
          } catch (e) {
            console.warn('Не удалось исправить кодировку имени файла');
          }
        }
      }
    } catch (decodeErr) {
      console.warn('Ошибка декодирования имени файла, используем оригинал:', decodeErr);
    }

    const fileType = path.extname(filename).toLowerCase() === '.csv' ? 'CSV' : 'XLSX';
    const uploadedBy = req.body.uploadedBy || 'System';
    let reportType = req.body.reportType || 'other'; // debtors, balance_sheet, other

    // Автоматическое определение типа отчета по названию файла
    const filenameLower = filename.toLowerCase();
    if (req.body.detailedReportType === 'balance_sheet_76') {
      reportType = 'balance_sheet_76';
    } else if (filenameLower.includes('оборотн') || filenameLower.includes('сальд') || filenameLower.includes('ведомост')) {
      reportType = 'balance_sheet';
      if (filenameLower.includes('76') || filenameLower.includes('76.06') || filenameLower.includes('счету 76')) {
        reportType = 'balance_sheet_76';
      }
    } else if (filenameLower.includes('должник') || filenameLower.includes('задолженност')) {
      reportType = 'debtors';
    }

    // Дополнительно: по содержимому (для файлов вроде "Новый.csv")
    if (reportType === 'other' && req.file.path) {
      try {
        const head = fs.readFileSync(req.file.path, { encoding: 'utf8', flag: 'r' }).slice(0, 2048);
        if (head.includes('Отчет по задолженности')) {
          reportType = 'debtors';
        }
      } catch (e) {
        // игнорируем ошибку чтения
      }
    }

    // Создаем запись об отчете в БД
    let reportResult;
    try {
      reportResult = await query(
        `INSERT INTO financial_reports (filename, file_type, uploaded_by, status, report_type)
         VALUES ($1, $2, $3, 'processing', $4)
         RETURNING id`,
        [filename, fileType, uploadedBy, reportType]
      );
    } catch (insertErr) {
      if (insertErr.code === '23514' && reportType === 'balance_sheet_76') {
        try {
          await ensureBalanceSheet76ReportType();
          reportResult = await query(
            `INSERT INTO financial_reports (filename, file_type, uploaded_by, status, report_type)
             VALUES ($1, $2, $3, 'processing', $4)
             RETURNING id`,
            [filename, fileType, uploadedBy, reportType]
          );
        } catch (migrateErr) {
          console.warn('[upload-report] Миграция ОСВ 76 не применена:', migrateErr.message);
          return res.status(400).json({
            error: 'Тип отчёта «ОСВ по счёту 76» не поддерживается текущей базой данных.',
            code: 'REPORT_TYPE_NOT_SUPPORTED',
            hint: 'Примените миграцию add_report_type_balance_sheet_76.sql и create_report_76_tables.sql (или перезапустите приложение).'
          });
        }
      } else {
        throw insertErr;
      }
    }

    const reportId = reportResult[0].id;

    // Если это ОСВ по счёту 76.06 (лицевые счета жителей)
    if (reportType === 'balance_sheet_76') {
      try {
        await ensureReport76Tables();
        const rows = await balanceSheet76Processor.parseBalanceSheet76(req.file.path, fileType);
        const client = await pool.connect();
        try {
          await client.query('BEGIN');
          for (let i = 0; i < rows.length; i++) {
            const r = rows[i];
            await client.query(
              `INSERT INTO report_76_rows (report_id, row_index, account_label, account_ls, saldo_start_debet, turnover_debet, turnover_credit, saldo_end_debet, saldo_end_credit)
               VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
              [
                reportId,
                r.row_index,
                r.account_label,
                r.account_ls || null,
                r.saldo_start_debet,
                r.turnover_debet,
                r.turnover_credit,
                r.saldo_end_debet,
                r.saldo_end_credit
              ]
            );
          }
          await client.query(
            `UPDATE financial_reports SET status = $1, processed_rows = $2, total_rows = $3 WHERE id = $4`,
            ['completed', rows.length, rows.length, reportId]
          );
          await client.query('COMMIT');
        } catch (err) {
          await client.query('ROLLBACK');
          throw err;
        } finally {
          client.release();
        }
        fs.unlink(req.file.path, (err) => { if (err) console.error('Ошибка удаления файла:', err); });
        return res.json({
          success: true,
          reportId,
          message: 'ОСВ по счёту 76 (лицевые счета) обработана успешно',
          rowsProcessed: rows.length
        });
      } catch (error) {
        console.error('Ошибка обработки ОСВ 76:', error);
        await query(
          'UPDATE financial_reports SET status = $1, error_log = $2 WHERE id = $3',
          ['failed', JSON.stringify([{ message: error.message }]), reportId]
        );
        const isMissingTable = error.code === '42P01' || (error.message && String(error.message).includes('report_76_rows'));
        return res.status(isMissingTable ? 400 : 500).json({
          error: isMissingTable ? 'Таблица для ОСВ 76 не найдена в базе данных.' : 'Ошибка обработки ОСВ 76',
          details: error.message,
          ...(isMissingTable && { hint: 'Примените миграцию create_report_76_tables.sql или перезапустите приложение.' })
        });
      }
    }

    // Если это оборотно-сальдовая ведомость по счёту 20, используем специальный процессор
    if (reportType === 'balance_sheet') {
      try {
        // Парсим ведомость
        const buildings = await balanceSheetProcessor.parseBalanceSheet(req.file.path, fileType);
        
        // Извлекаем период из названия файла
        const period = balanceSheetProcessor.extractPeriodFromFilename(filename);
        
        // Преобразуем в формат для БД
        const financialData = balanceSheetProcessor.convertToBuildingFinancialData(
          buildings, 
          reportId, 
          period.start, 
          period.end
        );

        // Сохраняем данные в БД
        const client = await pool.connect();
        try {
          await client.query('BEGIN');
          
          let processedCount = 0;
          let notFoundAddresses = [];
          
          console.log(`[balance-sheet] Обработка ${financialData.length} домов из ведомости`);
          
          for (const data of financialData) {
            console.log(`[balance-sheet] Поиск дома по адресу: "${data.address}" (totalExpenses=${data.totalExpenses})`);
            
            let buildingResult = { rows: [] };
            const normalizedForMapping = (data.address || '').trim().toLowerCase().replace(/\s+/g, ' ');

            // 1. Сначала пробуем найти по таблице маппинга адресов (адрес из 1С/ОСВ → building_id)
            const mappingResult = await client.query(
              `SELECT building_id FROM doma_address_mappings WHERE LOWER(TRIM(doma_address)) = $1 LIMIT 1`,
              [normalizedForMapping]
            );
            if (mappingResult.rows.length > 0) {
              const buildingId = mappingResult.rows[0].building_id;
              buildingResult = await client.query(
                `SELECT id, data->>'districtId' as district_id, data->'passport'->>'address' as address 
                 FROM buildings WHERE id = $1`,
                [buildingId]
              );
              if (buildingResult.rows.length > 0) {
                console.log(`[balance-sheet] Дом найден по doma_address_mappings: "${data.address}" → building_id=${buildingId}`);
              }
            }

            // 2. Точное совпадение с адресом в buildings
            if (buildingResult.rows.length === 0) {
              buildingResult = await client.query(
                `SELECT id, data->>'districtId' as district_id, data->'passport'->>'address' as address 
                 FROM buildings 
                 WHERE data->'passport'->>'address' = $1`,
                [data.address]
              );
            }

            // 3. Без учета регистра и лишних пробелов
            if (buildingResult.rows.length === 0) {
              buildingResult = await client.query(
                `SELECT id, data->>'districtId' as district_id, data->'passport'->>'address' as address 
                 FROM buildings 
                 WHERE LOWER(TRIM(data->'passport'->>'address')) = $1`,
                [normalizedForMapping]
              );
            }

            // 4. Нормализация: убрать "ул.", "д.", запятые, лишние пробелы и сравнить
            if (buildingResult.rows.length === 0) {
              const normalizedAddr = data.address
                .replace(/\bул\.?\s*/gi, '')
                .replace(/\bд\.?\s*/gi, '')
                .replace(/,/g, ' ')
                .replace(/\s+/g, ' ')
                .trim()
                .toLowerCase();
              if (normalizedAddr.length > 3) {
                buildingResult = await client.query(
                  `SELECT id, data->>'districtId' as district_id, data->'passport'->>'address' as address 
                   FROM buildings 
                   WHERE LOWER(TRIM(REPLACE(REPLACE(REPLACE(data->'passport'->>'address', ',', ' '), 'ул.', ''), 'д.', ''))) = $1
                   OR LOWER(TRIM(data->'passport'->>'address')) LIKE $2`,
                  [normalizedAddr, `%${normalizedAddr.replace(/%/g, '\\%')}%`]
                );
              }
            }

            // 5. По основной части адреса (до запятой или точки)
            if (buildingResult.rows.length === 0) {
              const mainPart = data.address.split(/[,\.]/)[0].trim();
              if (mainPart.length > 5) {
                buildingResult = await client.query(
                  `SELECT id, data->>'districtId' as district_id, data->'passport'->>'address' as address 
                   FROM buildings 
                   WHERE data->'passport'->>'address' ILIKE $1`,
                  [`%${mainPart}%`]
                );
              }
            }

            if (buildingResult.rows.length === 0) {
              console.log(`[balance-sheet] ⚠ Дом не найден в базе, пропускаем: "${data.address}" (totalExpenses=${data.totalExpenses})`);
              notFoundAddresses.push(data.address);
              continue;
            }

            if (buildingResult.rows.length > 0) {
              const buildingId = buildingResult.rows[0].id;
              const foundAddress = buildingResult.rows[0].address;
              const districtId = buildingResult.rows[0].district_id;
              
              if (foundAddress !== data.address) {
                console.log(`[balance-sheet] Найден дом с похожим адресом: "${foundAddress}" (искали: "${data.address}")`);
              }
              
              if (districtId) {
                console.log(`[balance-sheet] Дом связан с участком: ${districtId}`);
              }
              
              // Сохраняем финансовые данные
              // Используем INSERT с ON CONFLICT для обновления только если это тот же отчет
              // Если период совпадает, но отчет другой - создается новая запись (данные накапливаются)
              await client.query(
                `INSERT INTO building_financial_data 
                 (building_id, report_id, period_start, period_end, period_type, 
                  total_income, income_by_items, total_expenses, expenses_by_items, balance, metadata)
                 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
                 ON CONFLICT (building_id, period_start, period_end, period_type)
                 DO UPDATE SET
                   report_id = EXCLUDED.report_id,
                   total_income = EXCLUDED.total_income,
                   income_by_items = EXCLUDED.income_by_items,
                   total_expenses = EXCLUDED.total_expenses,
                   expenses_by_items = EXCLUDED.expenses_by_items,
                   balance = EXCLUDED.balance,
                   metadata = EXCLUDED.metadata,
                   updated_at = NOW()`,
                [
                  buildingId,
                  reportId,
                  data.periodStart,
                  data.periodEnd,
                  data.periodType,
                  data.totalIncome,
                  JSON.stringify({}),
                  data.totalExpenses,
                  JSON.stringify(data.expensesByItems),
                  data.balance,
                  JSON.stringify(data.metadata || {})
                ]
              );
              processedCount++;
              console.log(`[balance-sheet] ✓ Сохранены данные для дома: "${foundAddress}"`);
            }
          }
          
          if (notFoundAddresses.length > 0) {
            console.warn(`[balance-sheet] Всего не найдено домов: ${notFoundAddresses.length}`);
            console.warn(`[balance-sheet] Адреса, которые не были найдены:`, notFoundAddresses);
          }
          
          console.log(`[balance-sheet] Обработано домов: ${processedCount} из ${financialData.length}`);

          await client.query('COMMIT');
          
          // Обновляем статус отчета
          const finalStatus = processedCount > 0 ? (processedCount === financialData.length ? 'completed' : 'partial') : 'failed';
          await query(
            `UPDATE financial_reports 
             SET status = $1, 
                 processed_rows = $2,
                 total_rows = $3,
                 error_rows = $4
             WHERE id = $5`,
            [finalStatus, processedCount, financialData.length, notFoundAddresses.length, reportId]
          );
          
          if (notFoundAddresses.length > 0) {
            // Сохраняем список не найденных адресов в error_log
            await query(
              `UPDATE financial_reports 
               SET error_log = $1
               WHERE id = $2`,
              [JSON.stringify({ 
                notFoundAddresses: notFoundAddresses,
                message: `Не найдено ${notFoundAddresses.length} домов из ${financialData.length}. Данные по этим адресам не были загружены. Создайте дома вручную в системе, затем загрузите отчет повторно.`
              }), reportId]
            );
          }

          // Удаляем файл
          fs.unlink(req.file.path, (err) => {
            if (err) console.error('Ошибка удаления файла:', err);
          });

          return res.json({
            success: true,
            reportId,
            message: 'Ведомость обработана успешно',
            buildingsProcessed: processedCount
          });
        } catch (error) {
          await client.query('ROLLBACK');
          throw error;
        } finally {
          client.release();
        }
      } catch (error) {
        console.error('Ошибка обработки ведомости:', error);
        await query(
          'UPDATE financial_reports SET status = $1, error_log = $2 WHERE id = $3',
          ['failed', JSON.stringify([{ message: error.message }]), reportId]
        );
        return res.status(500).json({
          error: 'Ошибка обработки ведомости',
          details: error.message
        });
      }
    }

    // Если это отчёт по должникам (задолженности), парсим и сохраняем в debtor_report_data
    if (reportType === 'debtors') {
      try {
        // Убедиться, что таблица debtor_report_data существует (миграция могла не успеть выполниться)
        const tableCheck = await query(
          `SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'debtor_report_data' LIMIT 1`
        );
        if (!tableCheck || tableCheck.length === 0) {
          const migrationPath = path.join(__dirname, 'migrations', 'create_debtor_report_data.sql');
          if (fs.existsSync(migrationPath)) {
            const migrationSQL = fs.readFileSync(migrationPath, 'utf8');
            const client = await pool.connect();
            try {
              await client.query(migrationSQL);
              console.log('[debtors] Таблица debtor_report_data создана из миграции');
            } finally {
              client.release();
            }
          } else {
            throw new Error('Таблица debtor_report_data не найдена. Перезапустите сервер или примените миграцию create_debtor_report_data.sql');
          }
        }

        const rows = await debtorReportProcessor.parseDebtorReport(req.file.path, fileType);
        const client = await pool.connect();
        try {
          await client.query('BEGIN');
          await client.query('DELETE FROM debtor_report_data WHERE report_id = $1', [reportId]);
          let processedRows = 0;
          for (const row of rows) {
            await client.query(
              `INSERT INTO debtor_report_data (report_id, row_index, account, responsible_name, object_address, months_debt, total_debt)
               VALUES ($1, $2, $3, $4, $5, $6, $7)`,
              [
                reportId,
                row.row_index,
                row.account,
                row.responsible_name || null,
                row.object_address || null,
                row.months_debt,
                row.total_debt
              ]
            );
            processedRows++;
          }
          await client.query('COMMIT');
          const finalStatus = processedRows > 0 ? 'completed' : 'failed';
          await query(
            `UPDATE financial_reports SET status = $1, total_rows = $2, processed_rows = $3, error_rows = $4 WHERE id = $5`,
            [finalStatus, rows.length, processedRows, rows.length - processedRows, reportId]
          );
        } catch (err) {
          await client.query('ROLLBACK');
          throw err;
        } finally {
          client.release();
        }
        try {
          fs.unlinkSync(req.file.path);
        } catch (e) {
          console.warn('Ошибка удаления файла после обработки должников:', e.message);
        }
        return res.json({
          success: true,
          reportId,
          message: 'Отчёт по должникам обработан',
          processedRows: rows.length
        });
      } catch (error) {
        console.error('Ошибка обработки отчёта по должникам:', error);
        await query(
          'UPDATE financial_reports SET status = $1, error_log = $2 WHERE id = $3',
          ['failed', JSON.stringify([{ message: error.message }]), reportId]
        );
        try {
          fs.unlink(req.file.path, () => {});
        } catch (e) {}
        return res.status(500).json({
          error: 'Ошибка обработки отчёта по должникам',
          details: error.message
        });
      }
    }

    // Получаем маппинг (по умолчанию или указанный)
    const mappingId = req.body.mappingId;
    let mapping;

    if (mappingId) {
      const mappingResult = await query(
        'SELECT * FROM financial_report_mappings WHERE id = $1',
        [mappingId]
      );
      if (mappingResult.length > 0) {
        const dbMapping = mappingResult[0];
        // Преобразуем column_mappings (snake_case из БД) в columnMappings (camelCase для кода)
        // column_mappings - это JSONB, PostgreSQL возвращает его как объект
        const columnMappings = dbMapping.column_mappings || dbMapping.columnMappings;
        mapping = {
          ...dbMapping,
          columnMappings: typeof columnMappings === 'object' && columnMappings !== null 
            ? columnMappings 
            : (typeof columnMappings === 'string' ? JSON.parse(columnMappings) : {})
        };
      }
    }

    // Если маппинг не указан, берем по умолчанию
    if (!mapping) {
      const defaultMappingResult = await query(
        'SELECT * FROM financial_report_mappings WHERE is_default = true LIMIT 1'
      );
      if (defaultMappingResult.length > 0) {
        const dbMapping = defaultMappingResult[0];
        // Преобразуем column_mappings (snake_case из БД) в columnMappings (camelCase для кода)
        // column_mappings - это JSONB, PostgreSQL возвращает его как объект
        const columnMappings = dbMapping.column_mappings || dbMapping.columnMappings;
        mapping = {
          ...dbMapping,
          columnMappings: typeof columnMappings === 'object' && columnMappings !== null 
            ? columnMappings 
            : (typeof columnMappings === 'string' ? JSON.parse(columnMappings) : {})
        };
      } else {
        // Создаем базовый маппинг по умолчанию
        mapping = {
          id: null,
          name: 'Default Mapping',
          columnMappings: {
            'Адрес': 'address',
            'Период': 'periodStart',
            'Доход': 'totalIncome',
            'Расход': 'totalExpenses',
            'Баланс': 'balance'
          }
        };
      }
    }

    // Проверяем, что маппинг корректен
    if (!mapping || !mapping.columnMappings || Object.keys(mapping.columnMappings).length === 0) {
      return res.status(400).json({ 
        error: 'Маппинг колонок не настроен. Пожалуйста, настройте маппинг перед загрузкой файла.' 
      });
    }

    // Запускаем обработку файла асинхронно
    const { jobId, processPromise } = fileProcessor.processFile(req.file.path, fileType, mapping, reportId);
    
    processPromise
      .then(result => {
        // Удаляем файл после обработки
        fs.unlink(req.file.path, (err) => {
          if (err) console.error('Ошибка удаления файла:', err);
        });
      })
      .catch(error => {
        console.error('Ошибка обработки файла:', error);
        // Обновляем статус отчета на failed
        query(
          'UPDATE financial_reports SET status = $1, error_log = $2 WHERE id = $3',
          ['failed', JSON.stringify([{ message: error.message }]), reportId]
        );
      });

    res.json({
      success: true,
      reportId,
      jobId,
      message: 'Файл загружен, обработка начата'
    });

  } catch (err) {
    console.error('Error uploading report:', err);
    console.error('Error stack:', err.stack);
    res.status(500).json({ 
      error: 'Failed to upload report', 
      details: err.message,
      stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
    });
  }
});

// GET /api/finance/processing-status/:jobId - статус обработки
app.get(`${API_PREFIX}/finance/processing-status/:jobId`, (req, res) => {
  const { jobId } = req.params;
  const status = fileProcessor.getJobStatus(jobId);
  
  if (!status) {
    return res.status(404).json({ error: 'Job not found' });
  }

  res.json(status);
});

// POST /api/finance/configure-mapping - настройка маппинга
app.post(`${API_PREFIX}/finance/configure-mapping`, async (req, res) => {
  try {
    const { name, description, columnMappings, isDefault } = req.body;

    if (!name || !columnMappings) {
      return res.status(400).json({ error: 'name и columnMappings обязательны' });
    }

    // Если устанавливаем как default, снимаем флаг с других
    if (isDefault) {
      await query('UPDATE financial_report_mappings SET is_default = false');
    }

    const result = await query(
      `INSERT INTO financial_report_mappings (name, description, column_mappings, is_default)
       VALUES ($1, $2, $3, $4)
       RETURNING *`,
      [name, description || null, JSON.stringify(columnMappings), isDefault || false]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating mapping:', err);
    res.status(500).json({ error: 'Failed to create mapping' });
  }
});

// GET /api/finance/mappings - список маппингов
app.get(`${API_PREFIX}/finance/mappings`, async (req, res) => {
  try {
    const rows = await query(
      'SELECT * FROM financial_report_mappings ORDER BY is_default DESC, created_at DESC'
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching mappings:', err);
    res.status(500).json({ error: 'Failed to fetch mappings' });
  }
});

// GET /api/finance/building/:buildingId - финансовая сводка по дому
app.get(`${API_PREFIX}/finance/building/:buildingId`, async (req, res) => {
  try {
    const { buildingId } = req.params;
    const { periodStart, periodEnd, periodType } = req.query;

    let queryText = `
      SELECT * FROM building_financial_data
      WHERE building_id = $1
    `;
    const params = [buildingId];

    if (periodStart) {
      queryText += ` AND period_start >= $${params.length + 1}`;
      params.push(periodStart);
    }
    if (periodEnd) {
      queryText += ` AND period_end <= $${params.length + 1}`;
      params.push(periodEnd);
    }
    if (periodType) {
      queryText += ` AND period_type = $${params.length + 1}`;
      params.push(periodType);
    }

    queryText += ' ORDER BY period_start DESC, period_end DESC';

    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching building financial data:', err);
    res.status(500).json({ error: 'Failed to fetch building financial data' });
  }
});

// GET /api/finance/reports - список всех финансовых отчетов
app.get(`${API_PREFIX}/finance/reports`, async (req, res) => {
  try {
    const { reportType, status } = req.query;
    console.log('GET /api/finance/reports - запрос с параметрами:', { reportType, status });
    
    // Проверяем наличие колонки report_type
    let hasReportTypeColumn = false;
    try {
      const checkColumn = await query(`
        SELECT column_name 
        FROM information_schema.columns 
        WHERE table_name = 'financial_reports' AND column_name = 'report_type'
      `);
      hasReportTypeColumn = checkColumn.length > 0;
      console.log('Колонка report_type существует:', hasReportTypeColumn);
    } catch (checkErr) {
      console.warn('Ошибка проверки колонки report_type:', checkErr);
    }
    
    let queryText = `
      SELECT 
        id,
        filename,
        file_type,
        ${hasReportTypeColumn ? 'report_type,' : 'NULL as report_type,'}
        uploaded_at,
        uploaded_by,
        status,
        total_rows,
        processed_rows,
        error_rows,
        mapping_id
      FROM financial_reports
      WHERE 1=1
    `;
    const params = [];

    if (reportType && hasReportTypeColumn) {
      queryText += ` AND report_type = $${params.length + 1}`;
      params.push(reportType);
      console.log('Добавлен фильтр по типу отчета:', reportType);
    } else if (reportType && !hasReportTypeColumn) {
      console.warn('Фильтр по типу отчета не применен: колонка report_type не существует');
    }

    if (status) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }

    queryText += ' ORDER BY uploaded_at DESC';

    console.log('Выполняется запрос:', queryText, 'с параметрами:', params);
    const rows = await query(queryText, params);
    console.log('Найдено отчетов:', rows.length);
    
    res.json(rows);
  } catch (err) {
    console.error('Error fetching financial reports:', err);
    res.status(500).json({ error: 'Failed to fetch reports', details: err.message });
  }
});

// GET /api/finance/buildings/:buildingId/personal-accounts - лицевые счета, привязанные к дому (для ОСВ 76)
app.get(`${API_PREFIX}/finance/buildings/:buildingId/personal-accounts`, async (req, res) => {
  try {
    const { buildingId } = req.params;
    const rows = await query(
      `SELECT id, building_id AS "buildingId", account_ls AS "accountLs", account_label AS "accountLabel", apartment, created_at AS "createdAt"
       FROM building_personal_account_mappings
       WHERE building_id = $1
       ORDER BY account_ls`,
      [buildingId]
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching building personal accounts:', err);
    res.status(500).json({ error: 'Ошибка загрузки лицевых счетов дома', details: err.message });
  }
});

// POST /api/finance/building-personal-account-mappings - добавить/обновить привязку лицевого счёта к дому
app.post(`${API_PREFIX}/finance/building-personal-account-mappings`, async (req, res) => {
  try {
    const { building_id, account_ls, account_label, apartment } = req.body || {};
    if (!building_id || !account_ls) {
      return res.status(400).json({ error: 'Укажите building_id и account_ls' });
    }
    await query(
      `INSERT INTO building_personal_account_mappings (building_id, account_ls, account_label, apartment, updated_at)
       VALUES ($1, $2, $3, $4, NOW())
       ON CONFLICT (building_id, account_ls) DO UPDATE SET
         account_label = COALESCE(EXCLUDED.account_label, building_personal_account_mappings.account_label),
         apartment = COALESCE(EXCLUDED.apartment, building_personal_account_mappings.apartment),
         updated_at = NOW()`,
      [building_id, String(account_ls).trim(), account_label && String(account_label).trim() || null, apartment && String(apartment).trim() || null]
    );
    const row = await query(
      `SELECT id, building_id AS "buildingId", account_ls AS "accountLs", account_label AS "accountLabel", apartment, created_at AS "createdAt"
       FROM building_personal_account_mappings WHERE building_id = $1 AND account_ls = $2`,
      [building_id, String(account_ls).trim()]
    );
    res.status(201).json(row[0] || { buildingId: building_id, accountLs: account_ls, accountLabel: account_label, apartment });
  } catch (err) {
    console.error('Error upserting building personal account mapping:', err);
    res.status(500).json({ error: 'Ошибка сохранения привязки', details: err.message });
  }
});

// GET /api/finance/buildings - список домов с финансовыми данными
app.get(`${API_PREFIX}/finance/buildings`, async (req, res) => {
  try {
    const { reportId } = req.query;
    let queryText = `
      SELECT 
        b.id,
        b.data->'passport'->>'address' as address,
        b.data->>'districtId' as district_id,
        CASE 
          WHEN d.name IS NOT NULL AND d.name != '' THEN d.name
          ELSE b.data->>'districtId'
        END as district_name,
        COALESCE(SUM(bfd.total_income), 0) as total_income,
        COALESCE(SUM(bfd.total_expenses), 0) as total_expenses,
        COALESCE(SUM(bfd.balance), 0) as balance,
        COUNT(bfd.id) as reports_count,
        MAX(bfd.period_start) as period_start,
        MAX(bfd.period_end) as period_end
      FROM buildings b
      INNER JOIN building_financial_data bfd ON b.id = bfd.building_id
      LEFT JOIN districts d ON d.id = b.data->>'districtId'
    `;
    const params = [];

    if (reportId) {
      queryText += ` WHERE bfd.report_id = $1`;
      params.push(reportId);
    }

    queryText += `
      GROUP BY b.id, b.data->>'districtId', b.data->'passport'->>'address', d.name
      ORDER BY COALESCE(NULLIF(d.name, ''), b.data->>'districtId'), b.data->'passport'->>'address'
    `;

    const rows = await query(queryText, params);
    
    // Логируем для отладки
    if (rows.length > 0) {
      console.log('[finance/buildings] Пример данных:', {
        district_id: rows[0].district_id,
        district_name: rows[0].district_name,
        address: rows[0].address
      });
    }
    
    res.json(rows);
  } catch (err) {
    console.error('Error fetching buildings with financial data:', err);
    res.status(500).json({ error: 'Failed to fetch buildings' });
  }
});

// GET /api/finance/reports/:reportId/debtor-rows - строки отчёта по должникам
app.get(`${API_PREFIX}/finance/reports/:reportId/debtor-rows`, async (req, res) => {
  try {
    const { reportId } = req.params;
    const { search, minDebt } = req.query;
    let queryText = `
      SELECT id, report_id AS "reportId", row_index AS "rowIndex", account,
             responsible_name AS "responsibleName", object_address AS "objectAddress",
             months_debt AS "monthsDebt", total_debt AS "totalDebt", created_at AS "createdAt"
      FROM debtor_report_data
      WHERE report_id = $1
    `;
    const params = [reportId];
    if (search && String(search).trim() !== '') {
      const term = '%' + String(search).trim().replace(/%/g, '\\%') + '%';
      queryText += ` AND (account ILIKE $${params.length + 1} OR responsible_name ILIKE $${params.length + 1} OR object_address ILIKE $${params.length + 1})`;
      params.push(term, term, term);
    }
    if (minDebt !== undefined && minDebt !== '') {
      const num = parseFloat(minDebt);
      if (!Number.isNaN(num)) {
        queryText += ` AND total_debt >= $${params.length + 1}`;
        params.push(num);
      }
    }
    queryText += ' ORDER BY row_index';
    const result = await query(queryText, params);
    res.json(result);
  } catch (err) {
    console.error('Error fetching debtor rows:', err);
    res.status(500).json({ error: 'Failed to fetch debtor rows', details: err.message });
  }
});

// GET /api/finance/reports/:reportId/balance-sheet-76-rows - строки ОСВ по счёту 76 (лицевые счета)
app.get(`${API_PREFIX}/finance/reports/:reportId/balance-sheet-76-rows`, async (req, res) => {
  try {
    const { reportId } = req.params;
    const { buildingId } = req.query;
    const reportRow = await query(
      'SELECT id, report_type, filename FROM financial_reports WHERE id = $1',
      [reportId]
    );
    if (!reportRow || reportRow.length === 0) {
      return res.status(404).json({ error: 'Отчёт не найден' });
    }
    if (reportRow[0].report_type !== 'balance_sheet_76') {
      return res.status(400).json({ error: 'Отчёт не является ОСВ по счёту 76' });
    }
    let queryText = `
      SELECT id, report_id AS "reportId", row_index AS "rowIndex", account_label AS "accountLabel", account_ls AS "accountLs",
             saldo_start_debet AS "saldoStartDebet", turnover_debet AS "turnoverDebet", turnover_credit AS "turnoverCredit",
             saldo_end_debet AS "saldoEndDebet", saldo_end_credit AS "saldoEndCredit"
      FROM report_76_rows
      WHERE report_id = $1
    `;
    const params = [reportId];
    if (buildingId && String(buildingId).trim() !== '') {
      queryText = `
        SELECT r.id, r.report_id AS "reportId", r.row_index AS "rowIndex", r.account_label AS "accountLabel", r.account_ls AS "accountLs",
               r.saldo_start_debet AS "saldoStartDebet", r.turnover_debet AS "turnoverDebet", r.turnover_credit AS "turnoverCredit",
               r.saldo_end_debet AS "saldoEndDebet", r.saldo_end_credit AS "saldoEndCredit"
        FROM report_76_rows r
        INNER JOIN building_personal_account_mappings m ON (m.account_ls = r.account_ls OR (r.account_ls IS NULL AND LOWER(TRIM(m.account_label)) = LOWER(TRIM(r.account_label))))
        WHERE r.report_id = $1 AND m.building_id = $2
        ORDER BY r.row_index
      `;
      params.push(buildingId);
    } else {
      queryText += ' ORDER BY row_index';
    }
    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching balance-sheet-76 rows:', err);
    res.status(500).json({ error: 'Ошибка загрузки строк ОСВ 76', details: err.message });
  }
});

// GET /api/finance/reports/:reportId/aggregated - агрегированный отчет по всем домам
app.get(`${API_PREFIX}/finance/reports/:reportId/aggregated`, async (req, res) => {
  try {
    const { reportId } = req.params;
    const { periodStart, periodEnd } = req.query;
    
    // Формируем запрос с фильтрацией по периоду, если указан
    let queryText = `
      SELECT 
        bfd.*,
        b.data->'passport'->>'address' as address,
        b.data->>'districtId' as district_id,
        b.data->'passport'->'general'->>'totalArea' as total_area,
        b.data->'passport'->'general'->>'livingArea' as living_area,
        b.data->'passport'->'general'->>'nonLivingArea' as non_living_area
      FROM building_financial_data bfd
      JOIN buildings b ON bfd.building_id = b.id
      WHERE bfd.report_id = $1
    `;
    const params = [reportId];
    
    if (periodStart) {
      queryText += ` AND bfd.period_start >= $${params.length + 1}`;
      params.push(periodStart);
    }
    if (periodEnd) {
      queryText += ` AND bfd.period_end <= $${params.length + 1}`;
      params.push(periodEnd);
    }
    
    // Получаем все финансовые данные по отчету
    const financialData = await query(queryText, params);
    
    if (financialData.length === 0) {
      return res.status(404).json({ error: 'Данные не найдены' });
    }
    
    // Агрегируем данные
    let totalIncome = 0;
    let totalExpenses = 0;
    let totalBalance = 0;
    let totalArea = 0;
    let totalLivingArea = 0;
    let totalNonLivingArea = 0;
    const aggregatedExpenses = {};
    const dataPeriodStart = financialData[0].period_start;
    const dataPeriodEnd = financialData[0].period_end;
    
    financialData.forEach(row => {
      totalIncome += parseFloat(row.total_income) || 0;
      totalExpenses += parseFloat(row.total_expenses) || 0;
      totalBalance += parseFloat(row.balance) || 0;
      totalArea += parseFloat(row.total_area) || 0;
      totalLivingArea += parseFloat(row.living_area) || 0;
      totalNonLivingArea += parseFloat(row.non_living_area) || 0;
      
      // Агрегируем расходы по статьям
      if (row.expenses_by_items) {
        const expenses = typeof row.expenses_by_items === 'string' 
          ? JSON.parse(row.expenses_by_items) 
          : row.expenses_by_items;
        
        for (const [key, value] of Object.entries(expenses)) {
          aggregatedExpenses[key] = (aggregatedExpenses[key] || 0) + parseFloat(value);
        }
      }
    });
    
    // Используем выбранный период или период из данных
    const reportPeriodStart = periodStart || (financialData.length > 0 ? dataPeriodStart : null);
    const reportPeriodEnd = periodEnd || (financialData.length > 0 ? dataPeriodEnd : null);
    
    res.json({
      periodStart: reportPeriodStart,
      periodEnd: reportPeriodEnd,
      totalIncome,
      totalExpenses,
      totalBalance,
      totalArea,
      totalLivingArea,
      totalNonLivingArea,
      buildingsCount: financialData.length,
      aggregatedExpenses,
      buildings: financialData.map(row => ({
        id: row.building_id,
        address: row.address,
        income: parseFloat(row.total_income) || 0,
        expenses: parseFloat(row.total_expenses) || 0,
        balance: parseFloat(row.balance) || 0,
        expensesByItems: typeof row.expenses_by_items === 'string' 
          ? JSON.parse(row.expenses_by_items) 
          : row.expenses_by_items
      }))
    });
  } catch (err) {
    console.error('Error fetching aggregated report:', err);
    res.status(500).json({ error: 'Failed to fetch aggregated report' });
  }
});

// GET /api/finance/reports/:reportId/districts/:districtId/aggregated - агрегированный отчет по участку
app.get(`${API_PREFIX}/finance/reports/:reportId/districts/:districtId/aggregated`, async (req, res) => {
  try {
    const { reportId, districtId } = req.params;
    const { periodStart, periodEnd } = req.query;
    
    // Получаем название участка
    const districtResult = await query(
      'SELECT name FROM districts WHERE id = $1',
      [districtId]
    );
    const districtName = districtResult.length > 0 ? districtResult[0].name : districtId;
    
    // Получаем все финансовые данные по отчету и участку
    const financialData = await query(
      `SELECT 
        bfd.*,
        b.data->'passport'->>'address' as address,
        b.data->'passport'->'general'->>'totalArea' as total_area,
        b.data->'passport'->'general'->>'livingArea' as living_area,
        b.data->'passport'->'general'->>'nonLivingArea' as non_living_area
      FROM building_financial_data bfd
      JOIN buildings b ON bfd.building_id = b.id
      WHERE bfd.report_id = $1 AND b.data->>'districtId' = $2`,
      [reportId, districtId]
    );
    
    if (financialData.length === 0) {
      return res.status(404).json({ error: 'Данные по участку не найдены' });
    }
    
    // Агрегируем данные
    let totalIncome = 0;
    let totalExpenses = 0;
    let totalBalance = 0;
    let totalArea = 0;
    let totalLivingArea = 0;
    let totalNonLivingArea = 0;
    const aggregatedExpenses = {};
    const dataPeriodStart = financialData[0].period_start;
    const dataPeriodEnd = financialData[0].period_end;
    
    financialData.forEach(row => {
      totalIncome += parseFloat(row.total_income) || 0;
      totalExpenses += parseFloat(row.total_expenses) || 0;
      totalBalance += parseFloat(row.balance) || 0;
      totalArea += parseFloat(row.total_area) || 0;
      totalLivingArea += parseFloat(row.living_area) || 0;
      totalNonLivingArea += parseFloat(row.non_living_area) || 0;
      
      // Агрегируем расходы по статьям
      if (row.expenses_by_items) {
        const expenses = typeof row.expenses_by_items === 'string' 
          ? JSON.parse(row.expenses_by_items) 
          : row.expenses_by_items;
        
        for (const [key, value] of Object.entries(expenses)) {
          aggregatedExpenses[key] = (aggregatedExpenses[key] || 0) + parseFloat(value);
        }
      }
    });
    
    // Используем выбранный период или период из данных
    const reportPeriodStart = periodStart || (financialData.length > 0 ? dataPeriodStart : null);
    const reportPeriodEnd = periodEnd || (financialData.length > 0 ? dataPeriodEnd : null);
    
    res.json({
      districtId,
      districtName,
      periodStart: reportPeriodStart,
      periodEnd: reportPeriodEnd,
      totalIncome,
      totalExpenses,
      totalBalance,
      totalArea,
      totalLivingArea,
      totalNonLivingArea,
      buildingsCount: financialData.length,
      aggregatedExpenses,
      buildings: financialData.map(row => ({
        id: row.building_id,
        address: row.address,
        income: parseFloat(row.total_income) || 0,
        expenses: parseFloat(row.total_expenses) || 0,
        balance: parseFloat(row.balance) || 0,
        expensesByItems: typeof row.expenses_by_items === 'string' 
          ? JSON.parse(row.expenses_by_items) 
          : row.expenses_by_items
      }))
    });
  } catch (err) {
    console.error('Error fetching district aggregated report:', err);
    res.status(500).json({ error: 'Failed to fetch district aggregated report' });
  }
});

// ========= СПРАВОЧНИК СТАТЕЙ РАСХОДОВ =========

// GET /api/finance/expense-categories - список категорий расходов
app.get(`${API_PREFIX}/finance/expense-categories`, async (req, res) => {
  try {
    const rows = await query(
      `SELECT id, name, code, description, sort_order, is_active, created_at, updated_at
       FROM expense_categories
       WHERE is_active = TRUE
       ORDER BY sort_order, name`
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching expense categories:', err);
    res.status(500).json({ error: 'Failed to fetch expense categories' });
  }
});

// POST /api/finance/expense-categories - создание категории
app.post(`${API_PREFIX}/finance/expense-categories`, async (req, res) => {
  try {
    const { name, code, description, sortOrder } = req.body;
    
    if (!name) {
      return res.status(400).json({ error: 'name обязателен' });
    }
    
    const result = await query(
      `INSERT INTO expense_categories (name, code, description, sort_order)
       VALUES ($1, $2, $3, $4)
       RETURNING *`,
      [name, code || null, description || null, sortOrder || 0]
    );
    
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating expense category:', err);
    if (err.code === '23505') { // Unique violation
      return res.status(400).json({ error: 'Категория с таким названием уже существует' });
    }
    res.status(500).json({ error: 'Failed to create expense category' });
  }
});

// PUT /api/finance/expense-categories/:id - обновление категории
app.put(`${API_PREFIX}/finance/expense-categories/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { name, code, description, sortOrder, isActive } = req.body;
    
    const result = await query(
      `UPDATE expense_categories
       SET name = $1, code = $2, description = $3, sort_order = $4, is_active = $5, updated_at = NOW()
       WHERE id = $6
       RETURNING *`,
      [name, code || null, description || null, sortOrder || 0, isActive !== undefined ? isActive : true, id]
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Категория не найдена' });
    }
    
    res.json(result[0]);
  } catch (err) {
    console.error('Error updating expense category:', err);
    res.status(500).json({ error: 'Failed to update expense category' });
  }
});

// DELETE /api/finance/expense-categories/:id - удаление категории
app.delete(`${API_PREFIX}/finance/expense-categories/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    
    // Проверяем, есть ли статьи в этой категории
    const itemsCheck = await query(
      'SELECT COUNT(*) as count FROM expense_items WHERE category_id = $1',
      [id]
    );
    
    if (parseInt(itemsCheck[0].count) > 0) {
      return res.status(400).json({ 
        error: 'Невозможно удалить категорию: в ней есть статьи расходов. Сначала удалите или переместите статьи.' 
      });
    }
    
    const result = await query(
      'DELETE FROM expense_categories WHERE id = $1 RETURNING id',
      [id]
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Категория не найдена' });
    }
    
    res.json({ success: true, message: 'Категория удалена' });
  } catch (err) {
    console.error('Error deleting expense category:', err);
    res.status(500).json({ error: 'Failed to delete expense category' });
  }
});

// GET /api/finance/expense-items - список статей расходов
app.get(`${API_PREFIX}/finance/expense-items`, async (req, res) => {
  try {
    const { categoryId } = req.query;
    let queryText = `
      SELECT ei.id, ei.category_id, ei.name, ei.code, ei.description, 
             ei.parent_item_id, ei.sort_order, ei.is_active, ei.created_at, ei.updated_at,
             ec.name as category_name
      FROM expense_items ei
      JOIN expense_categories ec ON ei.category_id = ec.id
      WHERE ei.is_active = TRUE
    `;
    const params = [];
    
    if (categoryId) {
      queryText += ' AND ei.category_id = $1';
      params.push(categoryId);
    }
    
    queryText += ' ORDER BY ec.sort_order, ec.name, ei.sort_order, ei.name';
    
    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching expense items:', err);
    res.status(500).json({ error: 'Failed to fetch expense items' });
  }
});

// POST /api/finance/expense-items - создание статьи
app.post(`${API_PREFIX}/finance/expense-items`, async (req, res) => {
  try {
    const { categoryId, name, code, description, parentItemId, sortOrder } = req.body;
    
    if (!categoryId || !name) {
      return res.status(400).json({ error: 'categoryId и name обязательны' });
    }
    
    const result = await query(
      `INSERT INTO expense_items (category_id, name, code, description, parent_item_id, sort_order)
       VALUES ($1, $2, $3, $4, $5, $6)
       RETURNING *`,
      [categoryId, name, code || null, description || null, parentItemId || null, sortOrder || 0]
    );
    
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating expense item:', err);
    if (err.code === '23505') { // Unique violation
      return res.status(400).json({ error: 'Статья с таким названием уже существует в этой категории' });
    }
    res.status(500).json({ error: 'Failed to create expense item' });
  }
});

// PUT /api/finance/expense-items/:id - обновление статьи
app.put(`${API_PREFIX}/finance/expense-items/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { categoryId, name, code, description, parentItemId, sortOrder, isActive } = req.body;
    
    const result = await query(
      `UPDATE expense_items
       SET category_id = $1, name = $2, code = $3, description = $4, 
           parent_item_id = $5, sort_order = $6, is_active = $7, updated_at = NOW()
       WHERE id = $8
       RETURNING *`,
      [categoryId, name, code || null, description || null, parentItemId || null, sortOrder || 0, isActive !== undefined ? isActive : true, id]
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Статья не найдена' });
    }
    
    res.json(result[0]);
  } catch (err) {
    console.error('Error updating expense item:', err);
    res.status(500).json({ error: 'Failed to update expense item' });
  }
});

// DELETE /api/finance/expense-items/:id - удаление статьи
app.delete(`${API_PREFIX}/finance/expense-items/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    
    // Проверяем, есть ли дочерние статьи
    const childrenCheck = await query(
      'SELECT COUNT(*) as count FROM expense_items WHERE parent_item_id = $1',
      [id]
    );
    
    if (parseInt(childrenCheck[0].count) > 0) {
      return res.status(400).json({ 
        error: 'Невозможно удалить статью: у неё есть дочерние статьи. Сначала удалите или переместите дочерние статьи.' 
      });
    }
    
    const result = await query(
      'DELETE FROM expense_items WHERE id = $1 RETURNING id',
      [id]
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Статья не найдена' });
    }
    
    res.json({ success: true, message: 'Статья удалена' });
  } catch (err) {
    console.error('Error deleting expense item:', err);
    res.status(500).json({ error: 'Failed to delete expense item' });
  }
});

// GET /api/finance/expense-directory/tree - иерархическая структура справочника
app.get(`${API_PREFIX}/finance/expense-directory/tree`, async (req, res) => {
  try {
    const categories = await query(
      `SELECT id, name, code, description, sort_order
       FROM expense_categories
       WHERE is_active = TRUE
       ORDER BY sort_order, name`
    );
    
    const items = await query(
      `SELECT id, category_id, name, code, description, parent_item_id, sort_order
       FROM expense_items
       WHERE is_active = TRUE
       ORDER BY sort_order, name`
    );
    
    // Формируем иерархическую структуру
    const tree = categories.map(cat => {
      const categoryItems = items
        .filter(item => item.category_id === cat.id && !item.parent_item_id)
        .map(item => {
          const children = items.filter(child => child.parent_item_id === item.id);
          return {
            ...item,
            children: children.length > 0 ? children : undefined
          };
        });
      
      return {
        ...cat,
        items: categoryItems
      };
    });
    
    res.json(tree);
  } catch (err) {
    console.error('Error fetching expense directory tree:', err);
    res.status(500).json({ error: 'Failed to fetch expense directory tree' });
  }
});

// POST /api/finance/expense-directory/initialize - инициализация справочника из списка
app.post(`${API_PREFIX}/finance/expense-directory/initialize`, async (req, res) => {
  try {
    // Список категорий и статей из предоставленного изображения
    const directoryData = [
      {
        name: 'Административно-управленческие расходы',
        items: [
          'Набор персонала',
          'Публикация вакансий',
          'Налоги',
          'Аренда автомобиля без экипажа',
          { name: 'Программное обеспечение / IT', children: [
            'Обслуживание операционной системы АСУС МКД, Управдог и...',
            'Обслуживание сайта ООО Дружба'
          ]},
          { name: 'Расчетно-кассовое обслуживание в Банке', children: [
            'Комиссия банку'
          ]},
          { name: 'Содержание офиса', children: [
            'Аренда помещения',
            'Канц. товар',
            'Обслуживание орг.',
            'Ремонт и заправка картриджа',
            'Содержание офиса. Тревожная кнопка',
            'Хоз. средства для...',
            'Вода'
          ]},
          { name: 'Транспортные услуги', children: [
            'Доставка'
          ]},
          'Услуги по управлению',
          'Амортизационная',
          'Взносы в ФСС от НС и...',
          'Инвестиционный...',
          'Командировочные расходы АУП',
          'Компенсация за использование личного транспорта',
          'Обучение (повышение квалификации)',
          'Оплата больничного',
          'Оплата труда',
          'Осмотры инженерного оборудования, конструктивных элементов МКД',
          'Страховые взносы',
          'Судебные издержки / работы с дебиторской задолженностью / юридическое сопровождение',
          'Топливные карты'
        ]
      },
      {
        name: 'Услуги по начислению и сбору платы за содержание, выгрузка лиц счетов в ГИС ЖКХ, регистрационный учет',
        items: [
          'Услуги СММ',
          { name: 'Услуги связи', children: [
            'Интернет в офисе',
            'Сотовая связь',
            'Телефон консьерж'
          ]}
        ]
      },
      {
        name: 'Благоустройство',
        items: [
          'Озеленение',
          'Обслуживание клумб/газонов',
          'Материалы',
          'Обслуживание',
          { name: 'Ремонт/установка контейнерных площадок/урн', children: [
            'Работа/услуги',
            'Вывоз КГМ (крупногабаритного мусора)'
          ]}
        ]
      },
      {
        name: 'Механизированная уборка и вывоз снега',
        items: [
          'Уборка снега экскаватор погрузчик'
        ]
      },
      {
        name: 'Обслуживание инженерного оборудования',
        items: [
          { name: 'Восстановление системы пожаротушения', children: [
            'Материалы',
            'Восстановление системы'
          ]},
          'Расходные материалы'
        ]
      },
      {
        name: 'Общестроительные',
        items: [
          'Материалы общестроительные'
        ]
      },
      {
        name: 'Общехозяйственные',
        items: [
          'Материалы общехозяйственные',
          { name: 'Обработка наледи на дороге', children: [
            'Материалы Обработка наледи на...'
          ]}
        ]
      },
      {
        name: 'Сантехнические',
        items: [
          'Материалы'
        ]
      },
      {
        name: 'Электротехнические',
        items: [
          'Замена...',
          'Материалы'
        ]
      },
      {
        name: 'Судебные издержки / работы с дебиторской',
        items: [
          'Судебные издержки/работа с...',
          'Государственная...',
          'Обзвон должников',
          'Почтовые расходы'
        ]
      },
      {
        name: 'Текущее обслуживание общедомового имущества',
        items: [
          { name: 'Косметический ремонт МОП (места общего пользования)', children: [
            'Косметический ремонт подъездов',
            'Покрасочные работы в подъезде',
            'Материалы Покрасочные работы...',
            'Работа/услуги Покрасочные работы в подъезде'
          ]},
          { name: 'Ремонт / замена / изоляция / сварочные работы трубопроводов', children: [
            'Сварочные работы',
            'Сварочные работы на стояке',
            'Работа/услуги Сварочные работы на стояке'
          ]},
          { name: 'Ремонт кровли', children: [
            'Материалы Ремонт кровли'
          ]},
          'Ремонт/замена/установка столбов, заборов, перекладин и досок',
          { name: 'Изготовление / замена стенда, таблички, наклейки и т.д.', children: [
            'Материалы Изготовление / замена стенда, таблички'
          ]}
        ]
      },
      {
        name: 'Техническое обслуживание конструктивных элементов и инженерных коммуникаций МКД, работы',
        items: [
          'Работа по заявкам и техническому...',
          'Компенсация за раз...',
          'Выполнение заявок ремонтные и плотничные работы',
          'Выполнение электромонтажных и сантехнических работ и заявок',
          { name: 'Консьерж', children: [
            'Контроль исполнения заявок и работа с ними в приложении'
          ]}
        ]
      },
      {
        name: 'Обслуживание финансовых таблиц',
        items: [
          'Проценты по кредитам/займам'
        ]
      },
      {
        name: 'Санитарное содержание МОП',
        items: [
          'Санитарное содержание'
        ]
      },
      {
        name: 'Сопровождение 1с / консультационные',
        items: []
      },
      {
        name: 'Убытки',
        items: [
          'Затопление'
        ]
      },
      {
        name: 'Услуги специализированных',
        items: [
          'Аварийно-диспетчерская служба - ЛайфТелеком',
          'Дератизация/дезинфекция в МОП',
          'Диспетчерская служба',
          'Обслуживание АППЗ и ДУ',
          'Обслуживание ЗПУ (домофон)',
          'Обслуживание контейнеров тип КМ-2-2',
          'Обслуживание лифтов',
          'Страхование ответственности (лифты)',
          'Уборка подъездов'
        ]
      },
      {
        name: 'Хозяйственные расходы',
        items: [
          'Хоз расходы (спец одежда, инвентарь, инструменты, и т.д.)',
          { name: 'Инвентарь для уборки', children: [
            'Материалы Инвентарь для уборки'
          ]},
          'Ремонт инструментов',
          'Спец одежда',
          'Расходы на приобретение спецодежды СИЗ для слесарей и...',
          'Расходы на приобретение спец одежды СИЗ'
        ]
      }
    ];
    
    const client = await pool.connect();
    try {
      await client.query('BEGIN');
      
      let sortOrder = 0;
      for (const categoryData of directoryData) {
        // Создаем категорию
        const categoryResult = await client.query(
          `INSERT INTO expense_categories (name, sort_order)
           VALUES ($1, $2)
           ON CONFLICT (name) DO UPDATE SET sort_order = EXCLUDED.sort_order
           RETURNING id`,
          [categoryData.name, sortOrder++]
        );
        
        const categoryId = categoryResult.rows[0].id;
        let itemSortOrder = 0;
        
        // Создаем статьи в категории
        for (const itemData of categoryData.items) {
          if (typeof itemData === 'string') {
            // Простая статья
            await client.query(
              `INSERT INTO expense_items (category_id, name, sort_order)
               VALUES ($1, $2, $3)
               ON CONFLICT (category_id, name) DO UPDATE SET sort_order = EXCLUDED.sort_order`,
              [categoryId, itemData, itemSortOrder++]
            );
          } else if (itemData.name) {
            // Статья с дочерними элементами
            const parentResult = await client.query(
              `INSERT INTO expense_items (category_id, name, sort_order)
               VALUES ($1, $2, $3)
               ON CONFLICT (category_id, name) DO UPDATE SET sort_order = EXCLUDED.sort_order
               RETURNING id`,
              [categoryId, itemData.name, itemSortOrder++]
            );
            
            const parentId = parentResult.rows[0].id;
            let childSortOrder = 0;
            
            // Создаем дочерние статьи
            if (itemData.children) {
              for (const childName of itemData.children) {
                await client.query(
                  `INSERT INTO expense_items (category_id, name, parent_item_id, sort_order)
                   VALUES ($1, $2, $3, $4)
                   ON CONFLICT (category_id, name) DO UPDATE SET sort_order = EXCLUDED.sort_order`,
                  [categoryId, childName, parentId, childSortOrder++]
                );
              }
            }
          }
        }
      }
      
      await client.query('COMMIT');
      res.json({ success: true, message: 'Справочник инициализирован' });
    } catch (err) {
      await client.query('ROLLBACK');
      throw err;
    } finally {
      client.release();
    }
  } catch (err) {
    console.error('Error initializing expense directory:', err);
    res.status(500).json({ error: 'Failed to initialize expense directory', details: err.message });
  }
});

// POST /api/finance/expense-directory/match - сопоставление статей из отчета со справочником
app.post(`${API_PREFIX}/finance/expense-directory/match`, async (req, res) => {
  try {
    const { expenseName } = req.body;
    
    if (!expenseName) {
      return res.status(400).json({ error: 'expenseName обязателен' });
    }
    
    // Ищем совпадения в справочнике (точное и частичное)
    const exactMatch = await query(
      `SELECT ei.id, ei.name, ei.category_id, ec.name as category_name
       FROM expense_items ei
       JOIN expense_categories ec ON ei.category_id = ec.id
       WHERE LOWER(ei.name) = LOWER($1) AND ei.is_active = TRUE
       LIMIT 1`,
      [expenseName]
    );
    
    if (exactMatch.length > 0) {
      return res.json({ matched: true, item: exactMatch[0] });
    }
    
    // Частичное совпадение
    const partialMatch = await query(
      `SELECT ei.id, ei.name, ei.category_id, ec.name as category_name
       FROM expense_items ei
       JOIN expense_categories ec ON ei.category_id = ec.id
       WHERE LOWER(ei.name) LIKE LOWER($1) AND ei.is_active = TRUE
       ORDER BY LENGTH(ei.name)
       LIMIT 5`,
      [`%${expenseName}%`]
    );
    
    res.json({ 
      matched: false, 
      suggestions: partialMatch 
    });
  } catch (err) {
    console.error('Error matching expense item:', err);
    res.status(500).json({ error: 'Failed to match expense item' });
  }
});

// POST /api/finance/balance-sheet/:reportId/generate-resident-reports - генерация отчетов для всех домов
app.post(`${API_PREFIX}/finance/balance-sheet/:reportId/generate-resident-reports`, async (req, res) => {
  try {
    const { reportId } = req.params;

    // Получаем отчет
    const reportResult = await query(
      'SELECT * FROM financial_reports WHERE id = $1 AND report_type = $2',
      [reportId, 'balance_sheet']
    );

    if (reportResult.length === 0) {
      return res.status(404).json({ error: 'Ведомость не найдена' });
    }

    const report = reportResult[0];

    // Получаем все дома с данными из этой ведомости
    const buildingsData = await query(
      `SELECT bfd.*, b.id as building_id, b.data->'passport'->>'address' as address
       FROM building_financial_data bfd
       JOIN buildings b ON bfd.building_id = b.id
       WHERE bfd.report_id = $1`,
      [reportId]
    );

    if (buildingsData.length === 0) {
      return res.status(404).json({ error: 'Данные по домам не найдены' });
    }

    const createdReports = [];

    for (const buildingData of buildingsData) {
      // Проверяем, существует ли уже отчет для этого дома и периода
      const existingReport = await query(
        `SELECT id FROM resident_reports 
         WHERE building_id = $1 AND period_start = $2 AND period_end = $3`,
        [buildingData.building_id, buildingData.period_start, buildingData.period_end]
      );

      if (existingReport.length > 0) {
        // Обновляем существующий отчет
        const reportId = existingReport[0].id;
        await updateResidentReportContent(reportId, buildingData, report);
        createdReports.push({ buildingId: buildingData.building_id, reportId, updated: true });
      } else {
        // Создаем новый отчет
        const month = formatMonthFromPeriod(buildingData.period_start, buildingData.period_end);
        const content = await generateResidentReportContent(buildingData, report);

        const newReport = await query(
          `INSERT INTO resident_reports 
           (building_id, month, period_start, period_end, status, content)
           VALUES ($1, $2, $3, $4, 'draft', $5)
           RETURNING id`,
          [
            buildingData.building_id,
            month,
            buildingData.period_start,
            buildingData.period_end,
            JSON.stringify(content)
          ]
        );

        createdReports.push({ 
          buildingId: buildingData.building_id, 
          reportId: newReport[0].id, 
          updated: false 
        });
      }
    }

    res.json({
      success: true,
      reportsCreated: createdReports.length,
      reports: createdReports
    });

  } catch (err) {
    console.error('Error generating resident reports:', err);
    res.status(500).json({ error: 'Failed to generate reports', details: err.message });
  }
});

// POST /api/finance/balance-sheet/:reportId/generate-resident-report/:buildingId - генерация отчета для конкретного дома
app.post(`${API_PREFIX}/finance/balance-sheet/:reportId/generate-resident-report/:buildingId`, async (req, res) => {
  try {
    const { reportId, buildingId } = req.params;

    // Получаем данные по дому из ведомости
    const buildingData = await query(
      `SELECT bfd.*, b.data->'passport'->>'address' as address
       FROM building_financial_data bfd
       JOIN buildings b ON bfd.building_id = b.id
       WHERE bfd.report_id = $1 AND bfd.building_id = $2`,
      [reportId, buildingId]
    );

    if (buildingData.length === 0) {
      return res.status(404).json({ error: 'Данные по дому не найдены' });
    }

    const data = buildingData[0];
    const report = await query('SELECT * FROM financial_reports WHERE id = $1', [reportId]);

    // Проверяем существующий отчет
    const existingReport = await query(
      `SELECT id FROM resident_reports 
       WHERE building_id = $1 AND period_start = $2 AND period_end = $3`,
      [buildingId, data.period_start, data.period_end]
    );

    let reportIdResult;
    if (existingReport.length > 0) {
      // Обновляем
      reportIdResult = existingReport[0].id;
      await updateResidentReportContent(reportIdResult, data, report[0]);
    } else {
      // Создаем новый
      const month = formatMonthFromPeriod(data.period_start, data.period_end);
      const content = await generateResidentReportContent(data, report[0]);

      const newReport = await query(
        `INSERT INTO resident_reports 
         (building_id, month, period_start, period_end, status, content)
         VALUES ($1, $2, $3, $4, 'draft', $5)
         RETURNING id`,
        [buildingId, month, data.period_start, data.period_end, JSON.stringify(content)]
      );

      reportIdResult = newReport[0].id;
    }

    res.json({
      success: true,
      reportId: reportIdResult,
      buildingId: buildingId
    });

  } catch (err) {
    console.error('Error generating resident report:', err);
    res.status(500).json({ error: 'Failed to generate report', details: err.message });
  }
});

// GET /api/finance/reports/:reportId/buildings/:buildingId/aggregated - агрегированный отчет по конкретному дому
app.get(`${API_PREFIX}/finance/reports/:reportId/buildings/:buildingId/aggregated`, async (req, res) => {
  try {
    const { reportId, buildingId } = req.params;
    const { periodStart, periodEnd } = req.query;
    
    // Формируем запрос с фильтрацией по периоду, если указан
    let queryText = `
      SELECT 
        bfd.*,
        b.data->'passport'->>'address' as address,
        b.data->'passport'->'general'->>'totalArea' as total_area,
        b.data->'passport'->'general'->>'livingArea' as living_area,
        b.data->'passport'->'general'->>'nonLivingArea' as non_living_area
      FROM building_financial_data bfd
      JOIN buildings b ON bfd.building_id = b.id
      WHERE bfd.report_id = $1 AND bfd.building_id = $2
    `;
    const params = [reportId, buildingId];
    
    if (periodStart) {
      queryText += ` AND bfd.period_start >= $${params.length + 1}`;
      params.push(periodStart);
    }
    if (periodEnd) {
      queryText += ` AND bfd.period_end <= $${params.length + 1}`;
      params.push(periodEnd);
    }
    
    // Получаем финансовые данные по дому из отчета
    const financialData = await query(queryText, params);
    
    if (financialData.length === 0) {
      return res.status(404).json({ error: 'Данные по дому не найдены' });
    }
    
    const data = financialData[0];
    
    // Парсим расходы по статьям
    const expensesByItems = typeof data.expenses_by_items === 'string' 
      ? JSON.parse(data.expenses_by_items) 
      : (data.expenses_by_items || {});
    
    // Используем выбранный период или период из данных
    const reportPeriodStart = periodStart || data.period_start;
    const reportPeriodEnd = periodEnd || data.period_end;
    
    res.json({
      periodStart: reportPeriodStart,
      periodEnd: reportPeriodEnd,
      totalIncome: parseFloat(data.total_income) || 0,
      totalExpenses: parseFloat(data.total_expenses) || 0,
      totalBalance: parseFloat(data.balance) || 0,
      totalArea: parseFloat(data.total_area) || 0,
      totalLivingArea: parseFloat(data.living_area) || 0,
      totalNonLivingArea: parseFloat(data.non_living_area) || 0,
      buildingsCount: 1,
      aggregatedExpenses: expensesByItems,
      title: `Отчет по дому: ${data.address}`,
      subtitle: data.address
    });
  } catch (err) {
    console.error('Error fetching building aggregated report:', err);
    res.status(500).json({ error: 'Failed to fetch building aggregated report' });
  }
});

// GET /api/finance/reports/:reportId/buildings/:buildingId/report - получение отчета по дому из ведомости (генерирует на лету, если нет)
app.get(`${API_PREFIX}/finance/reports/:reportId/buildings/:buildingId/report`, async (req, res) => {
  try {
    const { reportId, buildingId } = req.params;

    // Получаем данные по дому из ведомости (по report_id и building_id)
    let buildingData = await query(
      `SELECT bfd.*, b.data->'passport'->>'address' as address
       FROM building_financial_data bfd
       JOIN buildings b ON bfd.building_id = b.id
       WHERE bfd.report_id = $1 AND bfd.building_id = $2`,
      [reportId, buildingId]
    );

    let data = buildingData.length > 0 ? buildingData[0] : null;
    let dataFromFallback = false;

    // Если нет строки или данные нулевые (total_expenses=0 и пустые expenses_by_items) — пробуем взять последние ненулевые данные по дому из любой ведомости
    const hasNoData = !data || (
      (parseFloat(data.total_expenses) || 0) === 0 &&
      (!data.expenses_by_items || (typeof data.expenses_by_items === 'object' && Object.keys(data.expenses_by_items).length === 0))
    );
    if (hasNoData) {
      const fallback = await query(
        `SELECT bfd.*, b.data->'passport'->>'address' as address
         FROM building_financial_data bfd
         JOIN buildings b ON bfd.building_id = b.id
         WHERE bfd.building_id = $1
           AND (bfd.total_expenses > 0 OR (bfd.expenses_by_items IS NOT NULL AND bfd.expenses_by_items != '{}'::jsonb))
         ORDER BY bfd.period_start DESC
         LIMIT 1`,
        [buildingId]
      );
      if (fallback.length > 0) {
        data = fallback[0];
        dataFromFallback = true;
      }
    }

    if (!data) {
      return res.status(404).json({ error: 'Данные по дому не найдены в ведомости' });
    }

    const report = await query('SELECT * FROM financial_reports WHERE id = $1', [reportId]);
    const reportRow = report.length > 0 ? report[0] : null;
    if (!reportRow) {
      return res.status(404).json({ error: 'Ведомость не найдена' });
    }

    // Проверяем существующий отчет (не используем сохранённый контент с нулями — перегенерируем из актуальных данных)
    const existingReport = await query(
      `SELECT id, content FROM resident_reports 
       WHERE building_id = $1 AND period_start = $2 AND period_end = $3`,
      [buildingId, data.period_start, data.period_end]
    );

    let reportContent;
    let residentReportId = null;

    const existingHasZeros = existingReport.length > 0 && (() => {
      const c = existingReport[0].content;
      const content = typeof c === 'string' ? (() => { try { return JSON.parse(c); } catch (e) { return {}; } })() : c;
      const total = content?.totals?.totalExpenses ?? content?.stats?.fundsSpent ?? 0;
      const hasServices = content?.services?.length > 0 && content.services.some(s => (s.accrued || s.paid) > 0);
      return total === 0 && !hasServices;
    })();

    if (existingReport.length > 0 && !existingHasZeros && !dataFromFallback) {
      residentReportId = existingReport[0].id;
      reportContent = existingReport[0].content;
      if (typeof reportContent === 'string') {
        try {
          reportContent = JSON.parse(reportContent);
        } catch (e) {
          reportContent = await generateResidentReportContent(data, reportRow);
        }
      }
    } else {
      reportContent = await generateResidentReportContent(data, reportRow);
    }

    const month = formatMonthFromPeriod(data.period_start, data.period_end);

    res.json({
      id: residentReportId,
      buildingId: buildingId,
      address: data.address,
      month: month,
      periodStart: data.period_start,
      periodEnd: data.period_end,
      content: reportContent,
      dataFromFallback: dataFromFallback || undefined
    });

  } catch (err) {
    console.error('Error getting building report:', err);
    res.status(500).json({ error: 'Failed to get report', details: err.message });
  }
});

// Вспомогательные функции для генерации контента отчета
function formatMonthFromPeriod(start, end) {
  const startDate = new Date(start);
  const endDate = new Date(end);
  const months = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
                  'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
  
  if (startDate.getFullYear() === endDate.getFullYear() && 
      startDate.getMonth() === endDate.getMonth()) {
    return `${months[startDate.getMonth()]} ${startDate.getFullYear()}`;
  }
  
  return `${startDate.getFullYear()} год`;
}

async function generateResidentReportContent(buildingData, report) {
  const expensesByItems = buildingData.expenses_by_items || {};
  const metadata = buildingData.metadata || {};
  
  // Получаем информацию о доме для параметров
  const buildingInfo = await query(
    `SELECT data FROM buildings WHERE id = $1`,
    [buildingData.building_id]
  );
  
  const building = buildingInfo.length > 0 ? buildingInfo[0].data : null;
  const passport = building?.passport || {};
  const general = passport.general || {};
  
  // Рассчитываем период в месяцах
  const startDate = new Date(buildingData.period_start);
  const endDate = new Date(buildingData.period_end);
  const monthsDiff = (endDate.getFullYear() - startDate.getFullYear()) * 12 + 
                     (endDate.getMonth() - startDate.getMonth()) + 1;
  
  // Получаем площади из данных дома
  const residentialArea = general.livingArea || 0;
  const nonResidentialArea = general.nonLivingArea || 0;
  const totalArea = general.totalArea || (residentialArea + nonResidentialArea);
  
  // Получаем тариф из настроек дома или используем по умолчанию
  const tariff = building?.passport?.management?.tariffMaintenance || 25.00;
  // Получаем процент резервного фонда из настроек дома или используем по умолчанию
  const reserveFundPercent = building?.passport?.management?.reserveFund || 5.00;
  
  // Формируем структуру отчета
  const content = {
    parameters: {
      periodStart: buildingData.period_start,
      periodEnd: buildingData.period_end,
      building: buildingData.address,
      residentialArea: residentialArea,
      nonResidentialArea: nonResidentialArea,
      totalArea: totalArea
    },
    tariffs: {
      tariff: tariff,
      reserveFund: reserveFundPercent
    },
    services: generateServices(expensesByItems, buildingData, totalArea, monthsDiff, tariff),
    balance: calculateBalance(buildingData, totalArea, monthsDiff, tariff, expensesByItems),
    expenseItems: generateExpenseItems(metadata, expensesByItems, buildingData, totalArea, monthsDiff),
    totals: calculateTotals(buildingData, totalArea, monthsDiff, expensesByItems, metadata),
    financialResults: calculateFinancialResults(buildingData, totalArea, monthsDiff, tariff, expensesByItems)
  };

  return content;
}

function generateServices(expensesByItems, buildingData, totalArea, monthsDiff, tariff) {
  // Группируем расходы по услугам
  const services = [];
  let totalMaintenance = buildingData.total_expenses || 0;
  let totalNonResidential = 0;
  let totalParking = 0;
  let concierge = 0;
  let snowRemoval = 0;
  let reserveFund = 0;
  let videoSurveillance = 0;
  let barrierService = 0;

  for (const [key, amount] of Object.entries(expensesByItems)) {
    const keyLower = key.toLowerCase();
    if (keyLower.includes('консьерж')) concierge += amount;
    if (keyLower.includes('механизированная уборка') || keyLower.includes('снег') || keyLower.includes('вывоз снега')) {
      snowRemoval += amount;
    }
    if (keyLower.includes('резервный фонд') || keyLower.includes('резерв')) reserveFund += amount;
    if (keyLower.includes('нж') || keyLower.includes('нежил')) totalNonResidential += amount;
    if (keyLower.includes('парковк')) totalParking += amount;
    if (keyLower.includes('видеонаблюдени')) videoSurveillance += amount;
    if (keyLower.includes('шлагбаум')) barrierService += amount;
  }

  // Рассчитываем начисленное (исходя из тарифа и площади)
  const accrued = totalArea * tariff * monthsDiff;
  const paid = accrued * 0.95; // Пример, можно получить из реальных данных

  // СОДЕРЖАНИЕ ВСЕГО
  services.push({
    name: 'СОДЕРЖАНИЕ ВСЕГО',
    debt: 0,
    accrued: accrued,
    paid: paid,
    percentOfPlan: Math.round((paid / accrued) * 100 * 100) / 100,
    order: 1
  });

  // Содержание (основное)
  const maintenanceAmount = totalMaintenance - totalNonResidential - totalParking;
  services.push({
    name: 'Содержание',
    debt: 0,
    accrued: maintenanceAmount,
    paid: maintenanceAmount * 0.95,
    percentOfPlan: 95,
    order: 2
  });

  // Содержание НЖ
  if (totalNonResidential > 0) {
    services.push({
      name: 'Содержание НЖ (в том числе парковка)',
      debt: 0,
      accrued: totalNonResidential,
      paid: totalNonResidential * 0.95,
      percentOfPlan: 95,
      order: 3
    });
  }

  // Парковка отдельно
  if (totalParking > 0) {
    services.push({
      name: 'Содержание - парковка',
      debt: 0,
      accrued: totalParking,
      paid: totalParking * 0.95,
      percentOfPlan: 95,
      order: 4
    });
  }

  // Видеонаблюдение
  if (videoSurveillance > 0) {
    services.push({
      name: 'Видеонаблюдение',
      debt: 0,
      accrued: 0,
      paid: 0,
      percentOfPlan: 5,
      order: 5
    });
  }

  // Обслуживание шлагбаума
  if (barrierService > 0) {
    services.push({
      name: 'Обслуживание шлагбаума',
      debt: 0,
      accrued: 0,
      paid: 0,
      percentOfPlan: 6,
      order: 6
    });
  }

  // Консьерж
  if (concierge > 0) {
    services.push({
      name: 'Консьерж',
      debt: 0,
      accrued: concierge,
      paid: concierge * 0.95,
      percentOfPlan: 95,
      order: 7
    });
  }

  // Механизированная уборка
  if (snowRemoval > 0) {
    services.push({
      name: 'Механизированная уборка и вывоз снега',
      debt: 0,
      accrued: snowRemoval,
      paid: snowRemoval * 0.95,
      percentOfPlan: 95,
      order: 8
    });
  }

  // Резервный фонд
  if (reserveFund > 0) {
    services.push({
      name: 'Резервный фонд',
      debt: 0,
      accrued: reserveFund,
      paid: reserveFund * 0.95,
      percentOfPlan: 95,
      order: 9
    });
  }

  return services;
}

function generateExpenseItems(metadata, expensesByItems, buildingData, totalArea, monthsDiff) {
  const items = [];
  let itemNumber = 1;

  if (metadata.groups) {
    for (const [groupName, group] of Object.entries(metadata.groups)) {
      const perMonth = group.total / monthsDiff;
      const perSquareMeter = totalArea > 0 ? perMonth / totalArea : 0;
      
      const groupItem = {
        number: `${itemNumber}.`,
        name: groupName,
        perMonth: perMonth,
        total: group.total,
        perSquareMeter: perSquareMeter,
        children: []
      };

      let subItemNumber = 1;
      for (const [articleName, article] of Object.entries(group.articles)) {
        const articlePerMonth = article.amount / monthsDiff;
        const articlePerSquareMeter = totalArea > 0 ? articlePerMonth / totalArea : 0;
        
        const articleItem = {
          number: `${itemNumber}.${subItemNumber}.`,
          name: articleName,
          perMonth: articlePerMonth,
          total: article.amount,
          perSquareMeter: articlePerSquareMeter,
          children: []
        };

        if (article.details && article.details.length > 0) {
          let detailNumber = 1;
          for (const detail of article.details) {
            const detailPerMonth = detail.amount / monthsDiff;
            const detailPerSquareMeter = totalArea > 0 ? detailPerMonth / totalArea : 0;
            
            articleItem.children.push({
              number: `${itemNumber}.${subItemNumber}.${detailNumber}.`,
              name: detail.description,
              perMonth: detailPerMonth,
              total: detail.amount,
              perSquareMeter: detailPerSquareMeter
            });
            detailNumber++;
          }
        }

        groupItem.children.push(articleItem);
        subItemNumber++;
      }

      items.push(groupItem);
      itemNumber++;
    }
  } else {
    // Если нет групп в metadata, формируем из expensesByItems
    for (const [key, amount] of Object.entries(expensesByItems)) {
      const parts = key.split(' > ');
      if (parts.length >= 2) {
        const groupName = parts[0];
        const articleName = parts.slice(1).join(' > ');
        
        // Ищем существующую группу
        let groupItem = items.find(item => item.name === groupName);
        if (!groupItem) {
          groupItem = {
            number: `${itemNumber}.`,
            name: groupName,
            perMonth: 0,
            total: 0,
            perSquareMeter: 0,
            children: []
          };
          items.push(groupItem);
          itemNumber++;
        }
        
        const perMonth = amount / monthsDiff;
        const perSquareMeter = totalArea > 0 ? perMonth / totalArea : 0;
        
        groupItem.children.push({
          number: `${groupItem.number}${groupItem.children.length + 1}.`,
          name: articleName,
          perMonth: perMonth,
          total: amount,
          perSquareMeter: perSquareMeter,
          children: []
        });
        
        groupItem.total += amount;
        groupItem.perMonth = groupItem.total / monthsDiff;
        groupItem.perSquareMeter = totalArea > 0 ? groupItem.perMonth / totalArea : 0;
      }
    }
  }

  return items;
}

function calculateBalance(buildingData, totalArea, monthsDiff, tariff, expensesByItems) {
  // Рассчитываем начисленное
  const accruedTotal = totalArea * tariff * monthsDiff;
  
  // Рассчитываем поступившие (примерно 95% от начисленного)
  const receivedTotal = accruedTotal * 0.95;
  
  // Расходы на содержание
  const maintenanceExpenses = buildingData.total_expenses || 0;
  
  // Сальдо исходя из начисленных
  const fromAccrued = accruedTotal - maintenanceExpenses;
  
  // Сальдо исходя из поступивших средств
  const fromReceived = receivedTotal - maintenanceExpenses;
  
  // Резервный фонд - начислено
  const reserveFundAccrued = totalArea * (tariff * 0.05) * monthsDiff;
  // Резервный фонд - поступило
  const reserveFundReceived = reserveFundAccrued * 0.95;
  
  // Расходы резервного фонда
  let reserveFundExpenses = 0;
  for (const [key, amount] of Object.entries(expensesByItems)) {
    if (key.toLowerCase().includes('резервный фонд') || key.toLowerCase().includes('резерв')) {
      reserveFundExpenses += amount;
    }
  }
  
  // Сальдо резервного фонда
  const reserveFundFromAccrued = reserveFundAccrued - reserveFundExpenses;
  const reserveFundFromReceived = reserveFundReceived - reserveFundExpenses;
  
  return {
    fromAccrued: Math.round(fromAccrued * 100) / 100,
    fromReceived: Math.round(fromReceived * 100) / 100,
    reserveFundFromAccrued: Math.round(reserveFundFromAccrued * 100) / 100,
    reserveFundFromReceived: Math.round(reserveFundFromReceived * 100) / 100
  };
}

function calculateTotals(buildingData, totalArea, monthsDiff, expensesByItems, metadata) {
  const totalExpenses = buildingData.total_expenses || 0;
  
  // Находим перерасчет (механизированная уборка + часть резервного фонда)
  let recalculation = 0;
  let snowRemovalAmount = 0;
  let reserveFundAmount = 0;
  
  // Ищем механизированную уборку
  for (const [key, amount] of Object.entries(expensesByItems)) {
    const keyLower = key.toLowerCase();
    if (keyLower.includes('механизированная уборка') || keyLower.includes('вывоз снега') || keyLower.includes('уборка снега')) {
      snowRemovalAmount += amount;
    }
    if (keyLower.includes('резервный фонд') || keyLower.includes('резерв')) {
      reserveFundAmount += amount;
    }
  }
  
  // Перерасчет = механизированная уборка + часть резервного фонда (если есть)
  recalculation = snowRemovalAmount;
  // Часть резервного фонда может быть в перерасчете (примерно 10-20%)
  if (reserveFundAmount > 0 && snowRemovalAmount > 0) {
    recalculation += reserveFundAmount * 0.15; // Примерная доля
  }
  
  // НДС рассчитывается от суммы расходов (10%)
  const baseForVAT = totalExpenses;
  const vat = Math.round(baseForVAT * 0.1 * 100) / 100; // 10% НДС
  
  // Итого с перерасчетом без НДС
  const totalWithRecalculation = totalExpenses - recalculation;
  
  // Итого с перерасчетом с НДС
  const totalWithRecalculationWithVAT = totalWithRecalculation + vat;
  
  // Итого тариф (то же что и итого с перерасчетом с НДС)
  const totalTariff = totalWithRecalculationWithVAT;
  
  // Прочие доходы (можно получить из buildingData или рассчитать)
  const otherIncome = 0; // TODO: получить из реальных данных
  
  return {
    totalExpenses: Math.round(totalExpenses * 100) / 100,
    vat: Math.round(vat * 100) / 100,
    recalculation: Math.round(recalculation * 100) / 100,
    totalWithRecalculation: Math.round(totalWithRecalculation * 100) / 100,
    totalWithRecalculationWithVAT: Math.round(totalWithRecalculationWithVAT * 100) / 100,
    debtReturn: 0,
    totalTariff: Math.round(totalTariff * 100) / 100,
    otherIncome: Math.round(otherIncome * 100) / 100
  };
}

function calculateFinancialResults(buildingData, totalArea, monthsDiff, tariff, expensesByItems) {
  // Рассчитываем начисленное
  const accruedTotal = totalArea * tariff * monthsDiff;
  
  // Рассчитываем поступившие (примерно 95% от начисленного)
  const receivedTotal = accruedTotal * 0.95;
  
  // Расходы на содержание (без резервного фонда)
  let maintenanceExpenses = buildingData.total_expenses || 0;
  let reserveFundExpenses = 0;
  
  for (const [key, amount] of Object.entries(expensesByItems)) {
    const keyLower = key.toLowerCase();
    if (keyLower.includes('резервный фонд') || keyLower.includes('резерв')) {
      reserveFundExpenses += amount;
      maintenanceExpenses -= amount;
    }
  }
  
  // Финансовый результат по содержанию
  // Исходя из начисленных
  const maintenanceFromAccrued = accruedTotal - maintenanceExpenses;
  // Исходя из поступивших
  const maintenanceFromReceived = receivedTotal - maintenanceExpenses;
  
  // Финансовый результат по резервному фонду
  // Начислено на резервный фонд
  const reserveFundAccrued = totalArea * (tariff * 0.05) * monthsDiff; // 5% от тарифа
  // Поступило на резервный фонд
  const reserveFundReceived = reserveFundAccrued * 0.95;
  
  // Исходя из начисленных
  const reserveFundFromAccrued = reserveFundAccrued - reserveFundExpenses;
  // Исходя из поступивших
  const reserveFundFromReceived = reserveFundReceived - reserveFundExpenses;
  
  return {
    maintenanceFromAccrued: Math.round(maintenanceFromAccrued * 100) / 100,
    maintenanceFromReceived: Math.round(maintenanceFromReceived * 100) / 100,
    reserveFundFromAccrued: Math.round(reserveFundFromAccrued * 100) / 100,
    reserveFundFromReceived: Math.round(reserveFundFromReceived * 100) / 100
  };
}

async function updateResidentReportContent(reportId, buildingData, report) {
  const content = await generateResidentReportContent(buildingData, report);
  await query(
    `UPDATE resident_reports SET content = $1, updated_at = NOW() WHERE id = $2`,
    [JSON.stringify(content), reportId]
  );
}

// ========= ОФИС: ENDPOINTS =========

// GET /api/office/dashboard — полный дашборд офиса для продвинутой статистики и карточки сводки
app.get(`${API_PREFIX}/office/dashboard`, async (req, res) => {
  const now = new Date();
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
  const monthStartStr = monthStart.toISOString().slice(0, 10);
  const weekStart = new Date(now);
  weekStart.setDate(weekStart.getDate() - weekStart.getDay());
  weekStart.setHours(0, 0, 0, 0);
  const weekEnd = new Date(weekStart);
  weekEnd.setDate(weekEnd.getDate() + 7);
  const warrantyLimit = new Date(now);
  warrantyLimit.setDate(warrantyLimit.getDate() + 30);

  const defaultData = {
    companyNews: { total: 0, published: 0, draft: 0, pending: 0, publishedThisMonth: 0 },
    equipment: { total: 0, byCondition: { good: 0, fair: 0, poor: 0 }, warrantyExpiringSoon: 0 },
    repairRequests: { total: 0, open: 0, in_progress: 0, completed: 0 },
    supplyRequests: { total: 0, byStatus: {} },
    inventory: { totalItems: 0, lowStockCount: 0 },
    documents: { total: 0, incoming: 0, outgoing: 0, byStatus: {} },
    orders: { total: 0, byStatus: {} },
    meetings: { thisWeek: 0, upcoming: 0 },
    meetingRooms: { total: 0 },
    knowledgeBase: { categoriesCount: 0, articlesCount: 0 }
  };

  try {
    // 1. Новости компании
    let companyNews = defaultData.companyNews;
    try {
      const newsRes = await query(`
        SELECT
          COUNT(*) as total,
          COUNT(*) FILTER (WHERE status = 'published') as published,
          COUNT(*) FILTER (WHERE status = 'draft') as draft,
          COUNT(*) FILTER (WHERE status = 'pending') as pending,
          COUNT(*) FILTER (WHERE status = 'published' AND published_at >= $1) as published_this_month
        FROM company_news
      `, [monthStart]);
      if (newsRes.length > 0) {
        const r = newsRes[0];
        companyNews = {
          total: parseInt(r.total) || 0,
          published: parseInt(r.published) || 0,
          draft: parseInt(r.draft) || 0,
          pending: parseInt(r.pending) || 0,
          publishedThisMonth: parseInt(r.published_this_month) || 0
        };
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 2. Оборудование
    let equipment = defaultData.equipment;
    try {
      const eqRes = await query(`
        SELECT
          COUNT(*) as total,
          COUNT(*) FILTER (WHERE condition = 'good') as good,
          COUNT(*) FILTER (WHERE condition = 'fair') as fair,
          COUNT(*) FILTER (WHERE condition = 'poor') as poor,
          COUNT(*) FILTER (WHERE warranty_until IS NOT NULL AND warranty_until >= $1 AND warranty_until <= $2) as warranty_expiring_soon
        FROM office_equipment
      `, [now.toISOString().slice(0, 10), warrantyLimit.toISOString().slice(0, 10)]);
      if (eqRes.length > 0) {
        const r = eqRes[0];
        equipment = {
          total: parseInt(r.total) || 0,
          byCondition: {
            good: parseInt(r.good) || 0,
            fair: parseInt(r.fair) || 0,
            poor: parseInt(r.poor) || 0
          },
          warrantyExpiringSoon: parseInt(r.warranty_expiring_soon) || 0
        };
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 3. Заявки на ремонт
    let repairRequests = defaultData.repairRequests;
    try {
      const repRes = await query(`
        SELECT
          COUNT(*) as total,
          COUNT(*) FILTER (WHERE status = 'completed') as completed
        FROM office_repair_requests
      `);
      if (repRes.length > 0) {
        const r = repRes[0];
        const total = parseInt(r.total) || 0;
        const completed = parseInt(r.completed) || 0;
        repairRequests = {
          total,
          open: total - completed,
          in_progress: total - completed,
          completed
        };
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 4. Заявки на закупку (по статусам)
    let supplyRequests = defaultData.supplyRequests;
    try {
      const supCount = await query('SELECT COUNT(*) as total FROM office_supply_requests');
      const supByStatus = await query(`
        SELECT status, COUNT(*) as cnt FROM office_supply_requests GROUP BY status
      `);
      const byStatus = {};
      (supByStatus || []).forEach(row => { byStatus[row.status || 'other'] = parseInt(row.cnt) || 0; });
      supplyRequests = { total: parseInt(supCount[0]?.total) || 0, byStatus };
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 5. Инвентарь
    let inventory = defaultData.inventory;
    try {
      const invRes = await query(`
        SELECT
          COUNT(*) as total_items,
          COUNT(*) FILTER (WHERE (min_threshold IS NOT NULL AND quantity <= min_threshold) OR (min_threshold IS NULL AND quantity <= 0)) as low_stock
        FROM office_inventory
      `);
      if (invRes.length > 0) {
        const r = invRes[0];
        inventory = {
          totalItems: parseInt(r.total_items) || 0,
          lowStockCount: parseInt(r.low_stock) || 0
        };
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 6. Документооборот
    let documents = defaultData.documents;
    try {
      const docRes = await query(`
        SELECT
          COUNT(*) as total,
          COUNT(*) FILTER (WHERE document_type = 'incoming' OR letter_type = 'incoming') as incoming,
          COUNT(*) FILTER (WHERE document_type = 'outgoing' OR letter_type = 'outgoing') as outgoing
        FROM office_documents
      `);
      const docByStatus = await query(`SELECT status, COUNT(*) as cnt FROM office_documents GROUP BY status`);
      const docStatusMap = {};
      (docByStatus || []).forEach(row => { docStatusMap[row.status || 'other'] = parseInt(row.cnt) || 0; });
      if (docRes.length > 0) {
        const r = docRes[0];
        documents = {
          total: parseInt(r.total) || 0,
          incoming: parseInt(r.incoming) || 0,
          outgoing: parseInt(r.outgoing) || 0,
          byStatus: docStatusMap
        };
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 7. Заказы
    let orders = defaultData.orders;
    try {
      const ordCount = await query('SELECT COUNT(*) as total FROM office_orders');
      const ordByStatus = await query(`SELECT status, COUNT(*) as cnt FROM office_orders GROUP BY status`);
      const ordStatusMap = {};
      (ordByStatus || []).forEach(row => { ordStatusMap[row.status || 'other'] = parseInt(row.cnt) || 0; });
      orders = { total: parseInt(ordCount[0]?.total) || 0, byStatus: ordStatusMap };
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 8. Совещания (текущая неделя, предстоящие)
    let meetings = defaultData.meetings;
    try {
      const weekStartStr = weekStart.toISOString().slice(0, 10);
      const weekEndStr = weekEnd.toISOString().slice(0, 10);
      const nowStr = now.toISOString();
      const meetRes = await query(`
        SELECT
          COUNT(*) FILTER (WHERE start_time >= $1 AND start_time < $2) as this_week,
          COUNT(*) FILTER (WHERE start_time > $3) as upcoming
        FROM meetings
      `, [weekStartStr, weekEndStr, nowStr]);
      if (meetRes.length > 0) {
        const r = meetRes[0];
        meetings = {
          thisWeek: parseInt(r.this_week) || 0,
          upcoming: parseInt(r.upcoming) || 0
        };
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 9. Переговорные
    let meetingRooms = defaultData.meetingRooms;
    try {
      const mrRes = await query('SELECT COUNT(*) as total FROM meeting_rooms');
      if (mrRes.length > 0) meetingRooms = { total: parseInt(mrRes[0].total) || 0 };
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 10. База знаний
    let knowledgeBase = defaultData.knowledgeBase;
    try {
      const kbCat = await query('SELECT COUNT(*) as c FROM knowledge_base_categories');
      const kbArt = await query('SELECT COUNT(*) as c FROM knowledge_base_articles');
      knowledgeBase = {
        categoriesCount: kbCat.length > 0 ? parseInt(kbCat[0].c) || 0 : 0,
        articlesCount: kbArt.length > 0 ? parseInt(kbArt[0].c) || 0 : 0
      };
    } catch (e) { if (e.code !== '42P01') throw e; }

    res.json({
      success: true,
      data: {
        companyNews,
        equipment,
        repairRequests,
        supplyRequests,
        inventory,
        documents,
        orders,
        meetings,
        meetingRooms,
        knowledgeBase
      }
    });
  } catch (err) {
    console.error('[office/dashboard] Ошибка:', err);
    res.status(500).json({ success: false, error: err.message || 'Failed to get office dashboard' });
  }
});

// Ремонтные заявки
app.get(`${API_PREFIX}/office/repair-requests`, async (req, res) => {
  try {
    const { status, equipmentId, assignedTo } = req.query;
    let queryText = `
      SELECT r.*, e.name as equipment_name, e.type as equipment_type
      FROM office_repair_requests r
      LEFT JOIN office_equipment e ON r.equipment_id = e.id
      WHERE 1=1
    `;
    const params = [];

    if (status) {
      queryText += ` AND r.status = $${params.length + 1}`;
      params.push(status);
    }
    if (equipmentId) {
      queryText += ` AND r.equipment_id = $${params.length + 1}`;
      params.push(equipmentId);
    }
    if (assignedTo) {
      queryText += ` AND r.assigned_to = $${params.length + 1}`;
      params.push(assignedTo);
    }

    queryText += ' ORDER BY r.created_at DESC';

    const rows = await query(queryText, params);

    const normalizedRows = rows.map((row) => ({
      ...row,
      is_paid: row.is_paid || false,
      cost: row.cost || 0,
      cost_estimated: row.cost_estimated || false,
      invoice_id: row.invoice_id || null,
      invoice_url: row.invoice_url || null,
      expected_return_date: row.expected_return_date || null,
      waiting_delivery_deadline: row.waiting_delivery_deadline || null,
      waiting_delivery_contacts: row.waiting_delivery_contacts || null,
      taken_for_repair_deadline: row.taken_for_repair_deadline || null,
      taken_for_repair_contacts: row.taken_for_repair_contacts || null,
      agreed_contractor_price: row.agreed_contractor_price != null ? row.agreed_contractor_price : null
    }));

    res.json(normalizedRows);
  } catch (err) {
    console.error('Error fetching repair requests:', err);
    res.status(500).json({ error: 'Failed to fetch repair requests' });
  }
});

app.post(`${API_PREFIX}/office/repair-requests`, async (req, res) => {
  try {
    const { equipmentId, requesterName, description, priority, expectedReturnDate } = req.body;

    if (!equipmentId || !requesterName || !description) {
      return res.status(400).json({ error: 'equipmentId, requesterName и description обязательны' });
    }

    const result = await query(
      `INSERT INTO office_repair_requests (equipment_id, requester_name, description, priority, status, expected_return_date)
       VALUES ($1, $2, $3, $4, 'new', $5)
       RETURNING *`,
      [equipmentId, requesterName, description, priority || 'medium', expectedReturnDate || null]
    );

    // Статус имущества — неисправно, в истории — на ремонте с причиной
    const today = new Date().toISOString().slice(0, 10);
    try {
      await query(
        `UPDATE office_equipment SET condition = 'poor', updated_at = NOW() WHERE id = $1`,
        [equipmentId]
      );
      await query(
        `INSERT INTO office_equipment_history (equipment_id, event_type, event_date, reason)
         VALUES ($1, 'repair', $2, $3)`,
        [equipmentId, today, description]
      );
    } catch (histErr) {
      console.warn('Equipment condition/history update on repair request:', histErr.message);
    }

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating repair request:', err);
    res.status(500).json({ error: 'Failed to create repair request' });
  }
});

app.put(`${API_PREFIX}/office/repair-requests/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const updates = req.body;

    // Поддерживаем как camelCase, так и snake_case
    const isPaid = updates.isPaid !== undefined ? updates.isPaid : updates.is_paid;
    const cost = updates.cost !== undefined ? updates.cost : null;
    const costEstimated = updates.costEstimated !== undefined ? updates.costEstimated : updates.cost_estimated;
    const invoiceId = updates.invoiceId !== undefined ? updates.invoiceId : updates.invoice_id;
    const invoiceUrl = updates.invoiceUrl !== undefined ? updates.invoiceUrl : updates.invoice_url;

    const expectedReturnDate = updates.expectedReturnDate !== undefined ? updates.expectedReturnDate : updates.expected_return_date;
    const allowedFields = ['status', 'assigned_to', 'assignedTo', 'solution', 'cancel_reason', 'cancelReason', 'comments'];
    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    for (const field of allowedFields) {
      const dbField = field === 'assignedTo' ? 'assigned_to' : 
                      field === 'cancelReason' ? 'cancel_reason' : field;
      if (updates[field] !== undefined) {
        updateFields.push(`${dbField} = $${paramIndex}`);
        values.push(updates[field]);
        paramIndex++;
      }
    }
    if (expectedReturnDate !== undefined) {
      updateFields.push(`expected_return_date = $${paramIndex}`);
      values.push(expectedReturnDate || null);
      paramIndex++;
    }

    const waitingDeliveryDeadline = updates.waitingDeliveryDeadline !== undefined ? updates.waitingDeliveryDeadline : updates.waiting_delivery_deadline;
    const waitingDeliveryContacts = updates.waitingDeliveryContacts !== undefined ? updates.waitingDeliveryContacts : updates.waiting_delivery_contacts;
    const takenForRepairDeadline = updates.takenForRepairDeadline !== undefined ? updates.takenForRepairDeadline : updates.taken_for_repair_deadline;
    const takenForRepairContacts = updates.takenForRepairContacts !== undefined ? updates.takenForRepairContacts : updates.taken_for_repair_contacts;
    const agreedContractorPrice = updates.agreedContractorPrice !== undefined ? updates.agreedContractorPrice : updates.agreed_contractor_price;

    if (waitingDeliveryDeadline !== undefined) {
      updateFields.push(`waiting_delivery_deadline = $${paramIndex}`);
      values.push(waitingDeliveryDeadline || null);
      paramIndex++;
    }
    if (waitingDeliveryContacts !== undefined) {
      updateFields.push(`waiting_delivery_contacts = $${paramIndex}`);
      values.push(waitingDeliveryContacts || null);
      paramIndex++;
    }
    if (takenForRepairDeadline !== undefined) {
      updateFields.push(`taken_for_repair_deadline = $${paramIndex}`);
      values.push(takenForRepairDeadline || null);
      paramIndex++;
    }
    if (takenForRepairContacts !== undefined) {
      updateFields.push(`taken_for_repair_contacts = $${paramIndex}`);
      values.push(takenForRepairContacts || null);
      paramIndex++;
    }
    if (agreedContractorPrice !== undefined) {
      updateFields.push(`agreed_contractor_price = $${paramIndex}`);
      values.push(agreedContractorPrice != null && agreedContractorPrice !== '' ? parseFloat(agreedContractorPrice) : null);
      paramIndex++;
    }

    // Поля для платного ремонта
    if (isPaid !== undefined) {
      updateFields.push(`is_paid = $${paramIndex}`);
      values.push(isPaid);
      paramIndex++;
    }
    if (cost !== undefined && cost !== null) {
      updateFields.push(`cost = $${paramIndex}`);
      values.push(parseFloat(cost) || 0);
      paramIndex++;
    }
    if (costEstimated !== undefined) {
      updateFields.push(`cost_estimated = $${paramIndex}`);
      values.push(costEstimated);
      paramIndex++;
    }
    if (invoiceId !== undefined && invoiceId !== null) {
      updateFields.push(`invoice_id = $${paramIndex}`);
      values.push(invoiceId);
      paramIndex++;
    }
    if (invoiceUrl !== undefined && invoiceUrl !== null) {
      updateFields.push(`invoice_url = $${paramIndex}`);
      values.push(invoiceUrl);
      paramIndex++;
    }

    if (updates.status === 'in_progress' && !updates.started_at) {
      updateFields.push(`started_at = NOW()`);
    }
    if (updates.status === 'completed' && !updates.completed_at) {
      updateFields.push(`completed_at = NOW()`);
    }
    if (updates.status === 'canceled' && !updates.canceled_at) {
      updateFields.push(`canceled_at = NOW()`);
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    values.push(id);

    const result = await query(
      `UPDATE office_repair_requests 
       SET ${updateFields.join(', ')}
       WHERE id = $${paramIndex}
       RETURNING *`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Repair request not found' });
    }

    const row = result[0];
    try {
      const assignedTo = row.assigned_to || updates.assigned_to || updates.assignedTo;
      const opts = { type: 'repair_request_update', title: `Заявка на ремонт №${id}`, body: row.status ? `Статус: ${row.status}` : 'Обновление заявки', entityType: 'repair_request', entityId: String(id) };
      if (assignedTo) {
        const userIds = await notificationService.resolveEmployeeNamesToUserIds(pool, [assignedTo]);
        if (userIds.length > 0) {
          await notificationService.createNotificationForUserIds(pool, userIds, opts);
        } else {
          await notificationService.createNotificationForResponsibleZone(pool, 'office', 'repair', opts);
        }
      } else {
        await notificationService.createNotificationForResponsibleZone(pool, 'office', 'repair', opts);
      }
    } catch (notifErr) {
      console.warn('[notifications] repair request PUT:', notifErr.message);
    }
    res.json({
      ...row,
      waiting_delivery_deadline: row.waiting_delivery_deadline || null,
      waiting_delivery_contacts: row.waiting_delivery_contacts || null,
      taken_for_repair_deadline: row.taken_for_repair_deadline || null,
      taken_for_repair_contacts: row.taken_for_repair_contacts || null,
      agreed_contractor_price: row.agreed_contractor_price != null ? row.agreed_contractor_price : null
    });
  } catch (err) {
    console.error('Error updating repair request:', err);
    const isEnumError = err.code === '22P02' || (err.message && /invalid input value for enum|enum.*does not exist/i.test(err.message));
    const message = isEnumError
      ? 'Недопустимый статус. Выполните миграции: backend/migrations/add_repair_request_statuses.sql'
      : (err.message || 'Failed to update repair request');
    res.status(isEnumError ? 400 : 500).json({ error: message });
  }
});

// Оборудование
app.get(`${API_PREFIX}/office/equipment`, async (req, res) => {
  try {
    const { type, assignedTo } = req.query;
    let queryText = 'SELECT * FROM office_equipment WHERE 1=1';
    const params = [];

    if (type) {
      queryText += ` AND type = $${params.length + 1}`;
      params.push(type);
    }
    if (assignedTo) {
      queryText += ` AND assigned_to = $${params.length + 1}`;
      params.push(assignedTo);
    }

    queryText += ' ORDER BY name';

    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching equipment:', err);
    res.status(500).json({ error: 'Failed to fetch equipment' });
  }
});

app.post(`${API_PREFIX}/office/equipment`, async (req, res) => {
  try {
    const { name, type, brand, model, serialNumber, assignedTo, purchaseDate, warrantyUntil, condition, notes } = req.body;

    if (!name || !type) {
      return res.status(400).json({ error: 'name и type обязательны' });
    }

    const result = await query(
      `INSERT INTO office_equipment 
       (name, type, brand, model, serial_number, assigned_to, purchase_date, warranty_until, condition, notes)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
       RETURNING *`,
      [name, type, brand || null, model || null, serialNumber || null, assignedTo || null,
       purchaseDate || null, warrantyUntil || null, condition || 'good', notes || null]
    );
    const equipmentId = result[0].id;
    const today = new Date().toISOString().slice(0, 10);

    try {
      if (purchaseDate) {
        await query(
          `INSERT INTO office_equipment_history (equipment_id, event_type, event_date) VALUES ($1, 'purchase', $2)`,
          [equipmentId, purchaseDate]
        );
      }
      if (assignedTo) {
        await query(
          `INSERT INTO office_equipment_history (equipment_id, event_type, event_date, assigned_to) VALUES ($1, 'issue', $2, $3)`,
          [equipmentId, today, assignedTo]
        );
      }
    } catch (histErr) {
      console.error('Error writing equipment history on create:', histErr);
    }

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating equipment:', err);
    res.status(500).json({ error: 'Failed to create equipment' });
  }
});

app.put(`${API_PREFIX}/office/equipment/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { name, type, brand, model, serialNumber, assignedTo, purchaseDate, warrantyUntil, nextMaintenanceDate, condition, notes } = req.body;

    const updates = [];
    const values = [];
    let paramIndex = 1;

    if (name !== undefined) {
      updates.push(`name = $${paramIndex++}`);
      values.push(name);
    }
    if (type !== undefined) {
      updates.push(`type = $${paramIndex++}`);
      values.push(type);
    }
    if (brand !== undefined) {
      updates.push(`brand = $${paramIndex++}`);
      values.push(brand);
    }
    if (model !== undefined) {
      updates.push(`model = $${paramIndex++}`);
      values.push(model);
    }
    if (serialNumber !== undefined) {
      updates.push(`serial_number = $${paramIndex++}`);
      values.push(serialNumber);
    }
    if (assignedTo !== undefined) {
      updates.push(`assigned_to = $${paramIndex++}`);
      values.push(assignedTo);
    }
    if (purchaseDate !== undefined) {
      updates.push(`purchase_date = $${paramIndex++}`);
      values.push(purchaseDate);
    }
    if (warrantyUntil !== undefined) {
      updates.push(`warranty_until = $${paramIndex++}`);
      values.push(warrantyUntil);
    }
    if (nextMaintenanceDate !== undefined) {
      updates.push(`next_maintenance_date = $${paramIndex++}`);
      values.push(nextMaintenanceDate);
    }
    if (condition !== undefined) {
      updates.push(`condition = $${paramIndex++}`);
      values.push(condition);
    }
    if (notes !== undefined) {
      updates.push(`notes = $${paramIndex++}`);
      values.push(notes);
    }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updates.push(`updated_at = NOW()`);
    values.push(id);

    const result = await query(
      `UPDATE office_equipment 
       SET ${updates.join(', ')}
       WHERE id = $${paramIndex}
       RETURNING *`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Оборудование не найдено' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating equipment:', err);
    res.status(500).json({ error: 'Failed to update equipment' });
  }
});

// История оборудования: закупка, выдача, перемещения, ремонты, списание
app.get(`${API_PREFIX}/office/equipment/:id/history`, async (req, res) => {
  try {
    const { id } = req.params;
    const histRows = await query(
      `SELECT id, equipment_id, event_type, event_date, assigned_to, assigned_from, reason, created_at
       FROM office_equipment_history
       WHERE equipment_id = $1
       ORDER BY event_date DESC, created_at DESC`,
      [id]
    );
    const repairs = await query(
      `SELECT id, equipment_id, description, solution, completed_at, created_at
       FROM office_repair_requests
       WHERE equipment_id = $1 AND status = 'completed' AND completed_at IS NOT NULL
       ORDER BY completed_at DESC`,
      [id]
    );
    const repairEvents = repairs.map((r) => ({
      id: `repair-${r.id}`,
      equipment_id: parseInt(id, 10),
      event_type: 'repair',
      event_date: r.completed_at ? r.completed_at.slice(0, 10) : r.created_at.slice(0, 10),
      reason: (r.solution || r.description) || '',
      created_at: r.completed_at || r.created_at
    }));
    const combined = [...histRows.map((h) => ({ ...h, equipment_id: parseInt(h.equipment_id, 10) })), ...repairEvents].sort(
      (a, b) => new Date(b.event_date).getTime() - new Date(a.event_date).getTime()
    );
    res.json(combined);
  } catch (err) {
    console.error('Error fetching equipment history:', err);
    res.status(500).json({ error: 'Failed to fetch equipment history' });
  }
});

// Перемещение: сменить ответственного и записать в историю
app.post(`${API_PREFIX}/office/equipment/:id/transfer`, async (req, res) => {
  try {
    const { id } = req.params;
    const assignedTo = req.body.assignedTo !== undefined ? req.body.assignedTo : req.body.assigned_to;
    const current = await query('SELECT id, assigned_to FROM office_equipment WHERE id = $1', [id]);
    if (!current.length) {
      return res.status(404).json({ error: 'Оборудование не найдено' });
    }
    const fromName = current[0].assigned_to || null;
    const toName = assignedTo !== undefined && assignedTo !== null && String(assignedTo).trim() !== '' ? String(assignedTo).trim() : null;
    const today = new Date().toISOString().slice(0, 10);
    await query(
      `INSERT INTO office_equipment_history (equipment_id, event_type, event_date, assigned_from, assigned_to)
       VALUES ($1, 'transfer', $2, $3, $4)`,
      [id, today, fromName, toName]
    );
    await query(
      `UPDATE office_equipment SET assigned_to = $1, updated_at = NOW() WHERE id = $2`,
      [toName, id]
    );
    const updated = await query('SELECT * FROM office_equipment WHERE id = $1', [id]);
    res.json(updated[0]);
  } catch (err) {
    console.error('Error transferring equipment:', err);
    res.status(500).json({ error: err.message || 'Failed to transfer equipment' });
  }
});

// Добавить событие в историю (списание, ручной ремонт и т.д.)
app.post(`${API_PREFIX}/office/equipment/:id/history`, async (req, res) => {
  try {
    const { id } = req.params;
    const { event_type, event_date, assigned_to, reason } = req.body;
    if (!event_type || !event_date) {
      return res.status(400).json({ error: 'event_type и event_date обязательны' });
    }
    const allowed = ['purchase', 'issue', 'transfer', 'repair', 'write_off'];
    if (!allowed.includes(event_type)) {
      return res.status(400).json({ error: 'Недопустимый event_type' });
    }
    await query(
      `INSERT INTO office_equipment_history (equipment_id, event_type, event_date, assigned_to, reason)
       VALUES ($1, $2, $3, $4, $5)`,
      [id, event_type, event_date, assigned_to || null, reason || null]
    );
    const rows = await query(
      'SELECT * FROM office_equipment_history WHERE equipment_id = $1 ORDER BY created_at DESC LIMIT 1',
      [id]
    );
    res.status(201).json(rows[0]);
  } catch (err) {
    console.error('Error adding equipment history:', err);
    res.status(500).json({ error: 'Failed to add equipment history' });
  }
});

// База знаний - категории
app.get(`${API_PREFIX}/office/knowledge-base/categories`, async (req, res) => {
  try {
    const rows = await query(
      'SELECT * FROM knowledge_base_categories ORDER BY sort_order, name'
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching categories:', err);
    res.status(500).json({ error: 'Failed to fetch categories' });
  }
});

app.post(`${API_PREFIX}/office/knowledge-base/categories`, async (req, res) => {
  try {
    const { name, description, parentId, sortOrder } = req.body;

    if (!name || !name.trim()) {
      return res.status(400).json({ error: 'name обязательно' });
    }

    const slug = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');

    const result = await query(
      `INSERT INTO knowledge_base_categories (name, slug, description, parent_id, sort_order)
       VALUES ($1, $2, $3, $4, $5)
       RETURNING *`,
      [name.trim(), slug, description || null, parentId || null, sortOrder || 0]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating category:', err);
    if (err.code === '23505') { // Unique violation
      return res.status(400).json({ error: 'Категория с таким названием уже существует' });
    }
    res.status(500).json({ error: 'Failed to create category' });
  }
});

// База знаний - статьи
app.get(`${API_PREFIX}/office/knowledge-base/articles`, async (req, res) => {
  try {
    const { categoryId, search, isPublished } = req.query;
    let queryText = `
      SELECT a.*, c.name as category_name
      FROM knowledge_base_articles a
      LEFT JOIN knowledge_base_categories c ON a.category_id = c.id
      WHERE 1=1
    `;
    const params = [];

    if (categoryId) {
      queryText += ` AND a.category_id = $${params.length + 1}`;
      params.push(categoryId);
    }
    if (search) {
      queryText += ` AND (a.title ILIKE $${params.length + 1} OR a.content ILIKE $${params.length + 1})`;
      const searchTerm = `%${search}%`;
      params.push(searchTerm, searchTerm);
    }
    if (isPublished !== undefined) {
      queryText += ` AND a.is_published = $${params.length + 1}`;
      params.push(isPublished === 'true');
    }

    queryText += ' ORDER BY a.created_at DESC';

    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching articles:', err);
    res.status(500).json({ error: 'Failed to fetch articles' });
  }
});

app.post(`${API_PREFIX}/office/knowledge-base/articles`, async (req, res) => {
  try {
    const { title, slug, categoryId, content, contentType, author, tags, attachments } = req.body;

    if (!title || !content || !author) {
      return res.status(400).json({ error: 'title, content и author обязательны' });
    }

    // Преобразуем массивы для PostgreSQL
    const tagsArray = Array.isArray(tags) ? tags : (tags ? [tags] : []);
    const attachmentsArray = Array.isArray(attachments) ? attachments : (attachments ? [attachments] : []);

    const result = await query(
      `INSERT INTO knowledge_base_articles 
       (title, slug, category_id, content, content_type, author, tags, attachments, is_published)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
       RETURNING *`,
      [
        title,
        slug || title.toLowerCase().replace(/\s+/g, '-'),
        categoryId || null,
        content,
        contentType || 'markdown',
        author,
        tagsArray.length > 0 ? tagsArray : null,
        attachmentsArray.length > 0 ? attachmentsArray : null,
        true
      ]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating article:', err);
    res.status(500).json({ error: 'Failed to create article' });
  }
});

// POST /api/office/knowledge-base/upload - загрузка файла для базы знаний
app.post(`${API_PREFIX}/office/knowledge-base/upload`, uploadKnowledgeBase.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'Файл не загружен' });
    }

    const fileUrl = `/uploads/knowledge-base/${req.file.filename}`;
    const fileInfo = {
      filename: req.file.originalname,
      url: fileUrl,
      size: req.file.size,
      mimetype: req.file.mimetype,
      uploadedAt: new Date().toISOString()
    };

    res.json({
      success: true,
      file: fileInfo
    });
  } catch (err) {
    console.error('Error uploading file:', err);
    res.status(500).json({ error: 'Failed to upload file' });
  }
});

app.put(`${API_PREFIX}/office/knowledge-base/articles/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const updates = req.body;

    // Поддерживаем как camelCase, так и snake_case
    const fieldMapping = {
      'categoryId': 'category_id',
      'contentType': 'content_type',
      'isPublished': 'is_published'
    };

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    // Обрабатываем поля
    if (updates.title !== undefined) {
      updateFields.push(`title = $${paramIndex++}`);
      values.push(updates.title);
    }
    if (updates.slug !== undefined) {
      updateFields.push(`slug = $${paramIndex++}`);
      values.push(updates.slug);
    }
    if (updates.categoryId !== undefined || updates.category_id !== undefined) {
      updateFields.push(`category_id = $${paramIndex++}`);
      const categoryId = updates.categoryId !== undefined ? updates.categoryId : updates.category_id;
      values.push(categoryId ? parseInt(categoryId) : null);
    }
    if (updates.content !== undefined) {
      updateFields.push(`content = $${paramIndex++}`);
      values.push(updates.content);
    }
    if (updates.contentType !== undefined || updates.content_type !== undefined) {
      updateFields.push(`content_type = $${paramIndex++}`);
      values.push(updates.contentType !== undefined ? updates.contentType : updates.content_type);
    }
    if (updates.tags !== undefined) {
      updateFields.push(`tags = $${paramIndex++}`);
      const tagsArray = Array.isArray(updates.tags) ? updates.tags : (updates.tags ? [updates.tags] : []);
      values.push(tagsArray.length > 0 ? tagsArray : null);
    }
    if (updates.attachments !== undefined) {
      updateFields.push(`attachments = $${paramIndex++}`);
      const attachmentsArray = Array.isArray(updates.attachments) ? updates.attachments : (updates.attachments ? [updates.attachments] : []);
      values.push(attachmentsArray.length > 0 ? attachmentsArray : null);
    }
    if (updates.isPublished !== undefined || updates.is_published !== undefined) {
      updateFields.push(`is_published = $${paramIndex++}`);
      values.push(updates.isPublished !== undefined ? updates.isPublished : updates.is_published);
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`, `version = version + 1`);
    values.push(id);

    const result = await query(
      `UPDATE knowledge_base_articles 
       SET ${updateFields.join(', ')}
       WHERE id = $${paramIndex}
       RETURNING *`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Article not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating article:', err);
    res.status(500).json({ error: 'Failed to update article' });
  }
});

// Совещания
app.get(`${API_PREFIX}/office/meetings`, async (req, res) => {
  try {
    const { startDate, endDate, status, organizer } = req.query;
    let queryText = `
      SELECT m.*, r.name as room_name, r.capacity as room_capacity
      FROM meetings m
      LEFT JOIN meeting_rooms r ON m.room_id = r.id
      WHERE 1=1
    `;
    const params = [];

    if (startDate && endDate && startDate === endDate) {
      // Если даты одинаковые, ищем совещания, которые начинаются в этот день
      queryText += ` AND DATE(m.start_time) = $${params.length + 1}`;
      params.push(startDate);
    } else {
      if (startDate) {
        queryText += ` AND m.start_time >= $${params.length + 1}`;
        params.push(startDate);
      }
      if (endDate) {
        // Добавляем 1 день к endDate, чтобы включить весь день
        const endDatePlusOne = new Date(endDate);
        endDatePlusOne.setDate(endDatePlusOne.getDate() + 1);
        queryText += ` AND m.start_time < $${params.length + 1}`;
        params.push(endDatePlusOne.toISOString().split('T')[0]);
      }
    }
    if (status) {
      queryText += ` AND m.status = $${params.length + 1}`;
      params.push(status);
    }
    if (organizer) {
      queryText += ` AND m.organizer = $${params.length + 1}`;
      params.push(organizer);
    }

    queryText += ' ORDER BY m.start_time';

    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching meetings:', err);
    res.status(500).json({ error: 'Failed to fetch meetings' });
  }
});

app.post(`${API_PREFIX}/office/meetings`, async (req, res) => {
  try {
    const { title, description, organizer, startTime, endTime, roomId, participants, agenda } = req.body;

    if (!title || !organizer || !startTime || !endTime) {
      return res.status(400).json({ error: 'title, organizer, startTime и endTime обязательны' });
    }

    // Если указана переговорная — проверяем, что на это время она не занята
    if (roomId) {
      const conflicts = await query(
        `SELECT id FROM meeting_bookings 
         WHERE room_id = $1 AND status = 'active'
         AND tstzrange(start_time, end_time) && tstzrange($2::timestamptz, $3::timestamptz)`,
        [roomId, startTime, endTime]
      );
      if (conflicts.length > 0) {
        return res.status(409).json({
          error: 'Переговорная на это время уже занята',
          code: 'ROOM_CONFLICT'
        });
      }
    }

    // Преобразуем массив участников для PostgreSQL
    const participantsArray = Array.isArray(participants) ? participants : (participants ? [participants] : []);

    const result = await query(
      `INSERT INTO meetings 
       (title, description, organizer, start_time, end_time, room_id, participants, agenda, status)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'scheduled')
       RETURNING *`,
      [
        title,
        description || null,
        organizer,
        startTime,
        endTime,
        roomId || null,
        participantsArray,
        agenda || null
      ]
    );

    // Создаем бронирование, если указана комната
    if (roomId) {
      await query(
        `INSERT INTO meeting_bookings (room_id, meeting_id, booked_by, start_time, end_time, status)
         VALUES ($1, $2, $3, $4, $5, 'active')`,
        [roomId, result[0].id, organizer, startTime, endTime]
      );
    }

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating meeting:', err);
    res.status(500).json({ error: 'Failed to create meeting' });
  }
});

// PUT /api/office/meetings/:id - обновление совещания
app.put(`${API_PREFIX}/office/meetings/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { title, description, startTime, endTime, roomId, participants, agenda, notes, conclusions, status } = req.body;

    const current = await query(
      'SELECT id, room_id, start_time, end_time FROM meetings WHERE id = $1',
      [id]
    );
    if (current.length === 0) {
      return res.status(404).json({ error: 'Meeting not found' });
    }
    const cur = current[0];
    const newStart = startTime !== undefined ? startTime : cur.start_time;
    const newEnd = endTime !== undefined ? endTime : cur.end_time;
    const newRoomId = roomId !== undefined ? (roomId || null) : cur.room_id;

    // При смене времени или комнаты — проверяем, что переговорная не занята (кроме текущего совещания)
    const timeOrRoomChanged = startTime !== undefined || endTime !== undefined || roomId !== undefined;
    if (timeOrRoomChanged && newRoomId) {
      const conflicts = await query(
        `SELECT id FROM meeting_bookings 
         WHERE room_id = $1 AND status = 'active'
         AND tstzrange(start_time, end_time) && tstzrange($2::timestamptz, $3::timestamptz)
         AND (meeting_id IS NULL OR meeting_id != $4)`,
        [newRoomId, newStart, newEnd, id]
      );
      if (conflicts.length > 0) {
        return res.status(409).json({
          error: 'Переговорная на это время уже занята',
          code: 'ROOM_CONFLICT'
        });
      }
    }

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (title !== undefined) {
      updates.push(`title = $${paramIndex++}`);
      params.push(title);
    }
    if (description !== undefined) {
      updates.push(`description = $${paramIndex++}`);
      params.push(description);
    }
    if (startTime !== undefined) {
      updates.push(`start_time = $${paramIndex++}`);
      params.push(startTime);
    }
    if (endTime !== undefined) {
      updates.push(`end_time = $${paramIndex++}`);
      params.push(endTime);
    }
    if (roomId !== undefined) {
      updates.push(`room_id = $${paramIndex++}`);
      params.push(roomId || null);
    }
    if (participants !== undefined) {
      updates.push(`participants = $${paramIndex++}`);
      const participantsArray = Array.isArray(participants) ? participants : (participants ? [participants] : []);
      params.push(participantsArray);
    }
    if (agenda !== undefined) {
      updates.push(`agenda = $${paramIndex++}`);
      params.push(agenda || null);
    }
    if (notes !== undefined) {
      updates.push(`notes = $${paramIndex++}`);
      params.push(notes || null);
    }
    if (conclusions !== undefined) {
      updates.push(`conclusions = $${paramIndex++}`);
      params.push(conclusions || null);
    }
    if (status !== undefined) {
      updates.push(`status = $${paramIndex++}`);
      params.push(status);
    }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updates.push(`updated_at = NOW()`);
    params.push(id);

    const result = await query(
      `UPDATE meetings 
       SET ${updates.join(', ')}
       WHERE id = $${paramIndex}
       RETURNING *`,
      params
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Meeting not found' });
    }

    // Синхронизируем бронирование переговорной при смене времени или комнаты
    if (timeOrRoomChanged) {
      if (newRoomId) {
        const upd = await query(
          `UPDATE meeting_bookings 
           SET room_id = $1, start_time = $2, end_time = $3, updated_at = NOW() 
           WHERE meeting_id = $4
           RETURNING id`,
          [newRoomId, newStart, newEnd, id]
        );
        if (upd.rows.length === 0) {
          await query(
            `INSERT INTO meeting_bookings (room_id, meeting_id, booked_by, start_time, end_time, status)
             VALUES ($1, $2, $3, $4, $5, 'active')`,
            [newRoomId, id, result[0].organizer || 'unknown', newStart, newEnd]
          );
        }
      } else {
        await query(
          `UPDATE meeting_bookings SET status = 'canceled', updated_at = NOW() WHERE meeting_id = $1`,
          [id]
        );
      }
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating meeting:', err);
    res.status(500).json({ error: 'Failed to update meeting' });
  }
});

// GET /api/office/meetings/:id - получение деталей совещания
app.get(`${API_PREFIX}/office/meetings/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    
    const result = await query(
      `SELECT m.*, r.name as room_name, r.capacity as room_capacity
       FROM meetings m
       LEFT JOIN meeting_rooms r ON m.room_id = r.id
       WHERE m.id = $1`,
      [id]
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Meeting not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error fetching meeting:', err);
    res.status(500).json({ error: 'Failed to fetch meeting' });
  }
});

// Переговорные комнаты
app.get(`${API_PREFIX}/office/meeting-rooms`, async (req, res) => {
  try {
    const { all } = req.query;
    const queryText = all === 'true' || all === '1'
      ? 'SELECT * FROM meeting_rooms ORDER BY is_active DESC, name'
      : 'SELECT * FROM meeting_rooms WHERE is_active = true ORDER BY name';
    const rows = await query(queryText);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching meeting rooms:', err);
    res.status(500).json({ error: 'Failed to fetch meeting rooms' });
  }
});

app.post(`${API_PREFIX}/office/meeting-rooms`, async (req, res) => {
  try {
    const { name, capacity, location, equipment, description } = req.body;

    if (!name || !name.trim()) {
      return res.status(400).json({ error: 'name обязательно' });
    }
    if (!capacity || capacity <= 0) {
      return res.status(400).json({ error: 'capacity должен быть больше 0' });
    }

    const equipmentArray = Array.isArray(equipment) ? equipment : (equipment ? [equipment] : []);

    const result = await query(
      `INSERT INTO meeting_rooms (name, capacity, location, equipment, description, is_active)
       VALUES ($1, $2, $3, $4, $5, true)
       RETURNING *`,
      [name.trim(), parseInt(capacity), location || null, equipmentArray.length > 0 ? equipmentArray : null, description || null]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating meeting room:', err);
    res.status(500).json({ error: 'Failed to create meeting room' });
  }
});

// GET /api/office/meeting-rooms/:id — одна переговорная (для редактирования)
app.get(`${API_PREFIX}/office/meeting-rooms/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query('SELECT * FROM meeting_rooms WHERE id = $1', [id]);
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Переговорная не найдена' });
    }
    res.json(rows[0]);
  } catch (err) {
    console.error('Error fetching meeting room:', err);
    res.status(500).json({ error: 'Failed to fetch meeting room' });
  }
});

// PUT /api/office/meeting-rooms/:id — обновление переговорной
app.put(`${API_PREFIX}/office/meeting-rooms/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { name, capacity, location, equipment, description, isActive } = req.body;

    const existing = await query('SELECT id FROM meeting_rooms WHERE id = $1', [id]);
    if (existing.length === 0) {
      return res.status(404).json({ error: 'Переговорная не найдена' });
    }

    if (name !== undefined && (!name || !name.toString().trim())) {
      return res.status(400).json({ error: 'name не может быть пустым' });
    }
    if (capacity !== undefined && (capacity == null || parseInt(capacity) <= 0)) {
      return res.status(400).json({ error: 'capacity должен быть больше 0' });
    }

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (name !== undefined) { updates.push(`name = $${paramIndex++}`); params.push(name.trim()); }
    if (capacity !== undefined) { updates.push(`capacity = $${paramIndex++}`); params.push(parseInt(capacity)); }
    if (location !== undefined) { updates.push(`location = $${paramIndex++}`); params.push(location || null); }
    if (description !== undefined) { updates.push(`description = $${paramIndex++}`); params.push(description || null); }
    if (equipment !== undefined) {
      const equipmentArray = Array.isArray(equipment) ? equipment : (equipment ? [equipment] : []);
      updates.push(`equipment = $${paramIndex++}`);
      params.push(equipmentArray.length > 0 ? equipmentArray : null);
    }
    if (isActive !== undefined) { updates.push(`is_active = $${paramIndex++}`); params.push(!!isActive); }

    if (updates.length === 0) {
      const rows = await query('SELECT * FROM meeting_rooms WHERE id = $1', [id]);
      return res.json(rows[0]);
    }

    updates.push(`updated_at = NOW()`);
    params.push(id);
    const result = await query(
      `UPDATE meeting_rooms SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      params
    );
    res.json(result[0]);
  } catch (err) {
    console.error('Error updating meeting room:', err);
    res.status(500).json({ error: 'Failed to update meeting room' });
  }
});

// Бронирования
app.get(`${API_PREFIX}/office/meeting-bookings`, async (req, res) => {
  try {
    const { roomId, startDate, endDate } = req.query;
    let queryText = `
      SELECT b.*, r.name as room_name, m.title as meeting_title
      FROM meeting_bookings b
      LEFT JOIN meeting_rooms r ON b.room_id = r.id
      LEFT JOIN meetings m ON b.meeting_id = m.id
      WHERE b.status = 'active'
    `;
    const params = [];

    if (roomId) {
      queryText += ` AND b.room_id = $${params.length + 1}`;
      params.push(roomId);
    }
    // Пересечение с диапазоном дат: бронь попадает в день/диапазон если пересекается с ним
    if (startDate && endDate) {
      queryText += ` AND b.start_time < ($${params.length + 2}::date + interval '1 day') AND b.end_time > $${params.length + 1}::date`;
      params.push(startDate, endDate);
    }

    queryText += ' ORDER BY b.start_time';

    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching bookings:', err);
    res.status(500).json({ error: 'Failed to fetch bookings' });
  }
});

app.post(`${API_PREFIX}/office/meeting-bookings`, async (req, res) => {
  try {
    const { roomId, bookedBy, startTime, endTime, purpose, meetingId } = req.body;

    if (!roomId || !bookedBy || !startTime || !endTime) {
      return res.status(400).json({ error: 'roomId, bookedBy, startTime и endTime обязательны' });
    }

    // Проверяем конфликты
    const conflicts = await query(
      `SELECT * FROM meeting_bookings 
       WHERE room_id = $1 
       AND status = 'active'
       AND tstzrange(start_time, end_time) && tstzrange($2, $3)`,
      [roomId, startTime, endTime]
    );

    if (conflicts.length > 0) {
      return res.status(409).json({ 
        error: 'Комната уже забронирована на это время',
        conflicts: conflicts
      });
    }

    const result = await query(
      `INSERT INTO meeting_bookings (room_id, meeting_id, booked_by, start_time, end_time, purpose, status)
       VALUES ($1, $2, $3, $4, $5, $6, 'active')
       RETURNING *`,
      [roomId, meetingId || null, bookedBy, startTime, endTime, purpose || null]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating booking:', err);
    res.status(500).json({ error: 'Failed to create booking' });
  }
});

// ========= ОФИС: ЗАЯВКИ НА ТМЦ =========

// GET /api/office/supply-requests - список заявок
app.get(`${API_PREFIX}/office/supply-requests`, async (req, res) => {
  try {
    const { status, category, requesterName } = req.query;
    let queryText = 'SELECT * FROM office_supply_requests WHERE 1=1';
    const params = [];

    if (status) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }
    if (category) {
      queryText += ` AND category = $${params.length + 1}`;
      params.push(category);
    }
    if (requesterName) {
      queryText += ` AND requester_name = $${params.length + 1}`;
      params.push(requesterName);
    }

    queryText += ' ORDER BY created_at DESC';

    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching supply requests:', err);
    res.status(500).json({ error: 'Failed to fetch supply requests' });
  }
});

// POST /api/office/supply-requests - создание заявки
app.post(`${API_PREFIX}/office/supply-requests`, async (req, res) => {
  try {
    const { requesterName, category, itemName, quantity, unit, amount, priority, notes } = req.body;

    if (!requesterName || !category || !itemName) {
      return res.status(400).json({ error: 'requesterName, category и itemName обязательны' });
    }

    const result = await query(
      `INSERT INTO office_supply_requests 
       (requester_name, category, item_name, quantity, unit, amount, priority, notes, status)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'new')
       RETURNING *`,
      [
        requesterName,
        category,
        itemName,
        quantity || 1,
        unit || 'шт.',
        amount || 0,
        priority || 'medium',
        notes || null
      ]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating supply request:', err);
    res.status(500).json({ error: 'Failed to create supply request' });
  }
});

// PUT /api/office/supply-requests/:id - обновление заявки
app.put(`${API_PREFIX}/office/supply-requests/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const updates = req.body;

    const allowedFields = ['status', 'approved_by', 'ordered_at', 'received_at', 'notes', 'priority', 'amount', 'issued_quantity'];
    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    for (const field of allowedFields) {
      if (updates[field] !== undefined) {
        updateFields.push(`${field} = $${paramIndex}`);
        values.push(updates[field]);
        paramIndex++;
      }
    }

    if (updates.status === 'approved' && !updates.approved_at) {
      updateFields.push(`approved_at = NOW()`);
    }
    if (updates.status === 'ordered' && !updates.ordered_at) {
      updateFields.push(`ordered_at = NOW()`);
    }
    if (updates.status === 'received' && !updates.received_at) {
      updateFields.push(`received_at = NOW()`);
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`);
    values.push(id);

    const result = await query(
      `UPDATE office_supply_requests 
       SET ${updateFields.join(', ')}
       WHERE id = $${paramIndex}
       RETURNING *`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Supply request not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating supply request:', err);
    res.status(500).json({ error: 'Failed to update supply request' });
  }
});

// DELETE /api/office/supply-requests/:id - удаление заявки
app.delete(`${API_PREFIX}/office/supply-requests/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM office_supply_requests WHERE id = $1 RETURNING *', [id]);

    if (result.length === 0) {
      return res.status(404).json({ error: 'Supply request not found' });
    }

    res.json({ message: 'Supply request deleted', id: result[0].id });
  } catch (err) {
    console.error('Error deleting supply request:', err);
    res.status(500).json({ error: 'Failed to delete supply request' });
  }
});

// ========= ОФИС: СКЛАД =========

// GET /api/office/inventory - список товаров на складе
app.get(`${API_PREFIX}/office/inventory`, async (req, res) => {
  try {
    const { category, search } = req.query;
    let queryText = 'SELECT * FROM office_inventory WHERE 1=1';
    const params = [];

    if (category) {
      queryText += ` AND category = $${params.length + 1}`;
      params.push(category);
    }
    if (search) {
      queryText += ` AND name ILIKE $${params.length + 1}`;
      params.push(`%${search}%`);
    }

    queryText += ' ORDER BY name';

    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching inventory:', err);
    res.status(500).json({ error: 'Failed to fetch inventory' });
  }
});

// POST /api/office/inventory - добавление товара
app.post(`${API_PREFIX}/office/inventory`, async (req, res) => {
  try {
    const { name, category, quantity, unit, minThreshold, location, notes } = req.body;

    if (!name) {
      return res.status(400).json({ error: 'name обязательно' });
    }

    const result = await query(
      `INSERT INTO office_inventory 
       (name, category, quantity, unit, min_threshold, location, notes)
       VALUES ($1, $2, $3, $4, $5, $6, $7)
       RETURNING *`,
      [
        name,
        category || null,
        quantity || 0,
        unit || 'шт.',
        minThreshold || 0,
        location || null,
        notes || null
      ]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating inventory item:', err);
    res.status(500).json({ error: 'Failed to create inventory item' });
  }
});

// PUT /api/office/inventory/:id - обновление товара
app.put(`${API_PREFIX}/office/inventory/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const updates = req.body;

    const allowedFields = ['name', 'category', 'quantity', 'unit', 'min_threshold', 'location', 'notes', 'last_restock', 'last_restock_by'];
    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    for (const field of allowedFields) {
      if (updates[field] !== undefined) {
        updateFields.push(`${field} = $${paramIndex}`);
        values.push(updates[field]);
        paramIndex++;
      }
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`);
    values.push(id);

    const result = await query(
      `UPDATE office_inventory 
       SET ${updateFields.join(', ')}
       WHERE id = $${paramIndex}
       RETURNING *`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Inventory item not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating inventory item:', err);
    res.status(500).json({ error: 'Failed to update inventory item' });
  }
});

// DELETE /api/office/inventory/:id - удаление товара
app.delete(`${API_PREFIX}/office/inventory/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM office_inventory WHERE id = $1 RETURNING *', [id]);

    if (result.length === 0) {
      return res.status(404).json({ error: 'Inventory item not found' });
    }

    res.json({ message: 'Inventory item deleted', id: result[0].id });
  } catch (err) {
    console.error('Error deleting inventory item:', err);
    res.status(500).json({ error: 'Failed to delete inventory item' });
  }
});

// ========= ОФИС: ДОКУМЕНТООБОРОТ =========

// GET /api/office/documents - список документов
app.get(`${API_PREFIX}/office/documents`, async (req, res) => {
  try {
    const { status, documentType, search, assignedTo } = req.query;
    let queryText = `
      SELECT 
        id, reg_number, title, correspondent, document_type, 
        COALESCE(letter_type, 'paper') as letter_type,
        status, date, assigned_to, tracking_number, file_url, 
        notes, created_by, created_at, updated_at
      FROM office_documents 
      WHERE 1=1
    `;
    const params = [];

    if (status) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }
    if (documentType) {
      queryText += ` AND document_type = $${params.length + 1}`;
      params.push(documentType);
    }
    if (assignedTo) {
      queryText += ` AND assigned_to = $${params.length + 1}`;
      params.push(assignedTo);
    }
    if (search) {
      queryText += ` AND (title ILIKE $${params.length + 1} OR reg_number ILIKE $${params.length + 1} OR correspondent ILIKE $${params.length + 1})`;
      const searchTerm = `%${search}%`;
      params.push(searchTerm, searchTerm, searchTerm);
    }

    queryText += ' ORDER BY date DESC, created_at DESC';

    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching documents:', err);
    res.status(500).json({ error: 'Failed to fetch documents' });
  }
});

// POST /api/office/documents/upload - загрузка файла для документов корреспонденции
app.post(`${API_PREFIX}/office/documents/upload`, uploadDocuments.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'Файл не загружен' });
    }

    // Исправляем кодировку оригинального имени файла
    let originalName = req.file.originalname;
    try {
      const hasCyrillic = /[А-Яа-яЁё]/.test(originalName);
      const hasGarbled = /[ÐÑ]/i.test(originalName) || /[â€]/i.test(originalName);
      
      if (hasGarbled || (!hasCyrillic && /[^\x00-\x7F]/.test(originalName))) {
        try {
          const buffer = Buffer.from(originalName, 'latin1');
          const decoded = buffer.toString('utf8');
          if (/[А-Яа-яЁё]/.test(decoded)) {
            originalName = decoded;
          }
        } catch (decodeErr) {
          try {
            originalName = decodeURIComponent(escape(originalName));
          } catch (e) {
            // Используем оригинал
          }
        }
      }
    } catch (decodeErr) {
      console.warn('Ошибка декодирования имени файла:', decodeErr);
    }

    // Используем API endpoint для скачивания вместо прямого пути
    const fileUrl = `/api/office/documents/download/${req.file.filename}`;
    const fileInfo = {
      filename: originalName,
      url: fileUrl,
      size: req.file.size,
      mimetype: req.file.mimetype,
      uploadedAt: new Date().toISOString(),
      storedFilename: req.file.filename // Сохраняем имя файла на диске
    };

    res.json({
      success: true,
      file: fileInfo,
      url: fileUrl,
      fileUrl: fileUrl
    });
  } catch (err) {
    console.error('Error uploading document file:', err);
    if (err.message && err.message.includes('Неподдерживаемый тип файла')) {
      return res.status(400).json({ error: err.message });
    }
    res.status(500).json({ error: 'Failed to upload file' });
  }
});

// GET /api/office/documents/download/:filename - скачивание файла документа
app.get(`${API_PREFIX}/office/documents/download/:filename`, async (req, res) => {
  try {
    const { filename } = req.params;
    const filePath = path.join(documentsDir, filename);
    
    // Проверяем существование файла
    if (!fs.existsSync(filePath)) {
      return res.status(404).json({ error: 'Файл не найден' });
    }

    // Получаем оригинальное имя файла из базы данных
    // Имя файла на диске имеет формат: UUID-безопасное_имя.расширение
    // Пытаемся извлечь оригинальное имя из имени файла на диске
    let originalFilename = filename;
    
    // Убираем UUID из начала имени файла
    const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/i;
    if (uuidPattern.test(filename)) {
      originalFilename = filename.replace(uuidPattern, '');
    }
    
    // Если имя файла содержит только подчеркивания вместо кириллицы, пытаемся получить из БД
    if (originalFilename.replace(/[^_]/g, '').length > 5) {
      try {
        // Ищем документ с этим файлом в базе данных
        const docResult = await query(
          'SELECT file_url, title FROM office_documents WHERE file_url LIKE $1 LIMIT 1',
          [`%${filename}%`]
        );
        
        // Пытаемся восстановить имя из названия документа или используем безопасное имя
        if (docResult.length > 0) {
          const ext = path.extname(filename);
          // Используем название документа как основу для имени файла
          if (docResult[0].title) {
            const safeTitle = docResult[0].title.replace(/[^a-zA-Z0-9_-]/g, '_').substring(0, 50);
            originalFilename = `${safeTitle}${ext}`;
          }
        }
      } catch (dbErr) {
        console.warn('Не удалось получить оригинальное имя файла из БД:', dbErr);
      }
    }

    // Определяем MIME тип файла
    const ext = path.extname(filePath).toLowerCase();
    const mimeTypes = {
      '.pdf': 'application/pdf',
      '.doc': 'application/msword',
      '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      '.txt': 'text/plain',
      '.jpg': 'image/jpeg',
      '.jpeg': 'image/jpeg',
      '.png': 'image/png',
      '.gif': 'image/gif',
      '.webp': 'image/webp'
    };
    const contentType = mimeTypes[ext] || 'application/octet-stream';
    
    // Устанавливаем правильные заголовки для скачивания
    res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(originalFilename)}"; filename*=UTF-8''${encodeURIComponent(originalFilename)}`);
    res.setHeader('Content-Type', contentType);
    
    // Отправляем файл (используем абсолютный путь)
    res.sendFile(path.resolve(filePath));
  } catch (err) {
    console.error('Error downloading document file:', err);
    res.status(500).json({ error: 'Failed to download file' });
  }
});

// GET /api/office/documents/next-reg-number - получение следующего регистрационного номера
app.get(`${API_PREFIX}/office/documents/next-reg-number`, async (req, res) => {
  try {
    const { documentType, date } = req.query;
    const docType = documentType || 'incoming';
    const docDate = date ? new Date(date) : new Date();
    const year = docDate.getFullYear();
    
    // Получаем последний номер для данного типа и года
    const prefix = docType === 'incoming' ? 'ВХ' : 'ИСХ';
    const yearStr = year.toString().slice(-2); // Последние 2 цифры года
    
    const result = await query(
      `SELECT reg_number 
       FROM office_documents 
       WHERE reg_number LIKE $1 
       ORDER BY reg_number DESC 
       LIMIT 1`,
      [`${prefix}-${yearStr}-%`]
    );
    
    let nextNumber = 1;
    if (result.length > 0) {
      const lastNumber = result[0].reg_number;
      const match = lastNumber.match(/-(\d+)$/);
      if (match) {
        nextNumber = parseInt(match[1]) + 1;
      }
    }
    
    const regNumber = `${prefix}-${yearStr}-${nextNumber.toString().padStart(4, '0')}`;
    res.json({ regNumber });
  } catch (err) {
    console.error('Error generating reg number:', err);
    res.status(500).json({ error: 'Failed to generate registration number' });
  }
});

// POST /api/office/documents - регистрация документа
app.post(`${API_PREFIX}/office/documents`, async (req, res) => {
  try {
    // Поддерживаем как camelCase, так и snake_case
    let { 
      regNumber, reg_number,
      title, 
      correspondent, 
      documentType, document_type,
      letterType, letter_type,
      date, 
      assignedTo, assigned_to,
      trackingNumber, tracking_number,
      fileUrl, file_url,
      notes, 
      createdBy, created_by
    } = req.body;
    
    // Нормализуем к camelCase
    regNumber = regNumber || reg_number;
    documentType = documentType || document_type;
    letterType = letterType || letter_type;
    assignedTo = assignedTo || assigned_to;
    trackingNumber = trackingNumber || tracking_number;
    fileUrl = fileUrl || file_url;
    createdBy = createdBy || created_by;

    // Если регистрационный номер не указан, генерируем автоматически
    if (!regNumber || !regNumber.trim()) {
      const docType = documentType || 'incoming';
      const docDate = date ? new Date(date) : new Date();
      const year = docDate.getFullYear();
      const prefix = docType === 'incoming' ? 'ВХ' : 'ИСХ';
      const yearStr = year.toString().slice(-2);
      
      const result = await query(
        `SELECT reg_number 
         FROM office_documents 
         WHERE reg_number LIKE $1 
         ORDER BY reg_number DESC 
         LIMIT 1`,
        [`${prefix}-${yearStr}-%`]
      );
      
      let nextNumber = 1;
      if (result.length > 0) {
        const lastNumber = result[0].reg_number;
        const match = lastNumber.match(/-(\d+)$/);
        if (match) {
          nextNumber = parseInt(match[1]) + 1;
        }
      }
      
      regNumber = `${prefix}-${yearStr}-${nextNumber.toString().padStart(4, '0')}`;
    }

    // Проверяем обязательные поля (regNumber уже сгенерирован выше, если не был указан)
    if (!title || !title.trim()) {
      return res.status(400).json({ error: 'title обязателен' });
    }
    if (!correspondent || !correspondent.trim()) {
      return res.status(400).json({ error: 'correspondent обязателен' });
    }
    if (!documentType) {
      return res.status(400).json({ error: 'documentType обязателен' });
    }
    if (!date) {
      return res.status(400).json({ error: 'date обязателен' });
    }
    if (!createdBy || !createdBy.trim()) {
      return res.status(400).json({ error: 'createdBy обязателен' });
    }

    // Если fileUrl содержит прямой путь к файлу, преобразуем его в API endpoint
    let finalFileUrl = fileUrl;
    if (fileUrl) {
      if (fileUrl.startsWith('/uploads/documents/')) {
        const filename = fileUrl.replace('/uploads/documents/', '');
        finalFileUrl = `/api/office/documents/download/${filename}`;
      } else if (fileUrl.startsWith('/api/office/documents/download/')) {
        // Уже правильный формат
        finalFileUrl = fileUrl;
      }
    }

    const result = await query(
      `INSERT INTO office_documents 
       (reg_number, title, correspondent, document_type, letter_type, date, assigned_to, tracking_number, file_url, notes, created_by, status)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'registered')
       RETURNING *`,
      [
        regNumber,
        title,
        correspondent,
        documentType,
        letterType || 'paper',
        date,
        assignedTo || null,
        trackingNumber || null,
        finalFileUrl || null,
        notes || null,
        createdBy
      ]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    if (err.code === '23505') { // unique_violation
      return res.status(409).json({ error: 'Документ с таким регистрационным номером уже существует' });
    }
    console.error('Error creating document:', err);
    res.status(500).json({ error: 'Failed to create document' });
  }
});

// PUT /api/office/documents/:id - обновление документа
app.put(`${API_PREFIX}/office/documents/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const updates = req.body;

    // Маппинг camelCase в snake_case для БД
    const fieldMapping = {
      'status': 'status',
      'assignedTo': 'assigned_to',
      'assigned_to': 'assigned_to',
      'trackingNumber': 'tracking_number',
      'tracking_number': 'tracking_number',
      'fileUrl': 'file_url',
      'file_url': 'file_url',
      'notes': 'notes',
      'title': 'title',
      'correspondent': 'correspondent',
      'documentType': 'document_type',
      'document_type': 'document_type',
      'letterType': 'letter_type',
      'letter_type': 'letter_type',
      'regNumber': 'reg_number',
      'reg_number': 'reg_number',
      'date': 'date'
    };

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    for (const [key, value] of Object.entries(updates)) {
      const dbField = fieldMapping[key];
      if (dbField && value !== undefined) {
        let processedValue = value;
        
        // Если обновляется file_url, преобразуем прямой путь в API endpoint
        if (dbField === 'file_url' && value) {
          if (value.startsWith('/uploads/documents/')) {
            const filename = value.replace('/uploads/documents/', '');
            processedValue = `/api/office/documents/download/${filename}`;
          }
        }
        
        updateFields.push(`${dbField} = $${paramIndex}`);
        values.push(processedValue);
        paramIndex++;
      }
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`);
    values.push(id);

    const result = await query(
      `UPDATE office_documents 
       SET ${updateFields.join(', ')}
       WHERE id = $${paramIndex}
       RETURNING *`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Document not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating document:', err);
    res.status(500).json({ error: 'Failed to update document' });
  }
});

// DELETE /api/office/documents/:id - удаление документа
app.delete(`${API_PREFIX}/office/documents/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM office_documents WHERE id = $1 RETURNING *', [id]);

    if (result.length === 0) {
      return res.status(404).json({ error: 'Document not found' });
    }

    res.json({ message: 'Document deleted', id: result[0].id });
  } catch (err) {
    console.error('Error deleting document:', err);
    res.status(500).json({ error: 'Failed to delete document' });
  }
});

// ========= ОФИС: ЗАКАЗЫ =========

// GET /api/office/orders - список заказов
app.get(`${API_PREFIX}/office/orders`, async (req, res) => {
  try {
    const { status } = req.query;
    let queryText = 'SELECT * FROM office_orders WHERE 1=1';
    const params = [];

    if (status) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }

    queryText += ' ORDER BY created_at DESC';

    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching orders:', err);
    res.status(500).json({ error: 'Failed to fetch orders' });
  }
});

// POST /api/office/orders - создание заказа
app.post(`${API_PREFIX}/office/orders`, async (req, res) => {
  try {
    const { title, description, requestIds, supplierName, supplierContact, expectedDate, notes, createdBy } = req.body;

    if (!title || !title.trim()) {
      return res.status(400).json({ error: 'title обязательно' });
    }
    if (!createdBy || !createdBy.trim()) {
      return res.status(400).json({ error: 'createdBy обязательно' });
    }

    // Генерируем номер заказа
    const now = new Date();
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, '0');
    const day = String(now.getDate()).padStart(2, '0');
    const timestamp = String(Date.now()).slice(-4);
    const orderNumber = `ORD-${year}-${month}${day}-${timestamp}`;

    // Создаем заказ
    const orderResult = await query(
      `INSERT INTO office_orders 
       (order_number, title, description, status, supplier_name, supplier_contact, expected_date, notes, created_by)
       VALUES ($1, $2, $3, 'waiting_quote', $4, $5, $6, $7, $8)
       RETURNING *`,
      [
        orderNumber,
        title.trim(),
        description || null,
        supplierName || null,
        supplierContact || null,
        expectedDate || null,
        notes || null,
        createdBy.trim()
      ]
    );

    const order = orderResult[0];

    // Добавляем позиции заказа из заявок
    if (requestIds && Array.isArray(requestIds) && requestIds.length > 0) {
      for (const requestId of requestIds) {
        // Получаем данные заявки
        const requestResult = await query('SELECT * FROM office_supply_requests WHERE id = $1', [requestId]);
        if (requestResult.length > 0) {
          const req = requestResult[0];
          const quantity = req.quantity || 1;
          const unitPrice = req.amount ? (req.amount / quantity) : 0;
          const totalPrice = req.amount || 0;

          await query(
            `INSERT INTO office_order_items (order_id, request_id, quantity, unit_price, total_price)
             VALUES ($1, $2, $3, $4, $5)`,
            [order.id, requestId, quantity, unitPrice, totalPrice]
          );
        }
      }

      // Пересчитываем общую сумму заказа
      const itemsResult = await query(
        'SELECT SUM(total_price) as total FROM office_order_items WHERE order_id = $1',
        [order.id]
      );
      const totalAmount = itemsResult[0]?.total || 0;

      await query(
        'UPDATE office_orders SET total_amount = $1 WHERE id = $2',
        [totalAmount, order.id]
      );

      order.total_amount = totalAmount;
    }

    res.status(201).json(order);
  } catch (err) {
    console.error('Error creating order:', err);
    res.status(500).json({ error: 'Failed to create order' });
  }
});

// PUT /api/office/orders/:orderId/items/:itemId - обновление позиции заказа (кол-во / цена / сумма)
app.put(`${API_PREFIX}/office/orders/:orderId/items/:itemId`, async (req, res) => {
  try {
    const { orderId, itemId } = req.params;
    const { quantity, unitPrice, totalPrice } = req.body;

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (quantity !== undefined) {
      updateFields.push(`quantity = $${paramIndex++}`);
      values.push(parseFloat(quantity) || 0);
    }
    if (unitPrice !== undefined) {
      updateFields.push(`unit_price = $${paramIndex++}`);
      values.push(parseFloat(unitPrice) || 0);
    }
    if (totalPrice !== undefined) {
      updateFields.push(`total_price = $${paramIndex++}`);
      values.push(parseFloat(totalPrice) || 0);
    }

    // Если итоговая сумма не передана, пересчитываем как quantity * unit_price
    if (!updateFields.some(f => f.startsWith('total_price'))) {
      updateFields.push(`total_price = (COALESCE(quantity, 0) * COALESCE(unit_price, 0))`);
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    values.push(itemId);

    const updatedItems = await query(
      `UPDATE office_order_items
       SET ${updateFields.join(', ')}
       WHERE id = $${paramIndex}
       RETURNING *`,
      values
    );

    if (updatedItems.length === 0) {
      return res.status(404).json({ error: 'Order item not found' });
    }

    // Синхронизируем цену в исходной заявке (office_supply_requests)
    const item = updatedItems[0];
    if (item.request_id) {
      await query(
        'UPDATE office_supply_requests SET amount = $1, updated_at = NOW() WHERE id = $2',
        [item.total_price || 0, item.request_id]
      );
    }

    // Пересчитываем общую сумму заказа
    const sumResult = await query(
      'SELECT SUM(total_price) as total FROM office_order_items WHERE order_id = $1',
      [orderId]
    );
    const totalAmount = sumResult[0]?.total || 0;

    await query(
      'UPDATE office_orders SET total_amount = $1, updated_at = NOW() WHERE id = $2',
      [totalAmount, orderId]
    );

    res.json({
      item: updatedItems[0],
      totalAmount
    });
  } catch (err) {
    console.error('Error updating order item:', err);
    res.status(500).json({ error: 'Failed to update order item' });
  }
});

// GET /api/office/orders/:id/items - позиции заказа
app.get(`${API_PREFIX}/office/orders/:id/items`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      `SELECT oi.*, osr.item_name, osr.category, osr.unit
       FROM office_order_items oi
       JOIN office_supply_requests osr ON oi.request_id = osr.id
       WHERE oi.order_id = $1`,
      [id]
    );
    res.json(rows.map((item) => ({
      id: item.id,
      orderId: item.order_id,
      requestId: item.request_id,
      request: {
        id: item.request_id,
        itemName: item.item_name,
        category: item.category,
        unit: item.unit
      },
      quantity: item.quantity,
      unitPrice: item.unit_price,
      totalPrice: item.total_price
    })));
  } catch (err) {
    console.error('Error fetching order items:', err);
    res.status(500).json({ error: 'Failed to fetch order items' });
  }
});

// GET /api/office/orders/:id/quotes - предложения для заказа
app.get(`${API_PREFIX}/office/orders/:id/quotes`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      'SELECT * FROM office_order_quotes WHERE order_id = $1 ORDER BY created_at DESC',
      [id]
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching order quotes:', err);
    res.status(500).json({ error: 'Failed to fetch order quotes' });
  }
});

// POST /api/office/orders/:id/quotes - добавить предложение
app.post(`${API_PREFIX}/office/orders/:id/quotes`, async (req, res) => {
  try {
    const { id } = req.params;
    const { supplierName, supplierContact, totalAmount, quoteFileUrl, notes } = req.body;

    if (!supplierName || !totalAmount) {
      return res.status(400).json({ error: 'supplierName и totalAmount обязательны' });
    }

    const result = await query(
      `INSERT INTO office_order_quotes 
       (order_id, supplier_name, supplier_contact, total_amount, quote_file_url, notes)
       VALUES ($1, $2, $3, $4, $5, $6)
       RETURNING *`,
      [id, supplierName, supplierContact || null, parseFloat(totalAmount), quoteFileUrl || null, notes || null]
    );

    // Обновляем статус заказа если это первое предложение
    const quotesCount = await query(
      'SELECT COUNT(*) as count FROM office_order_quotes WHERE order_id = $1',
      [id]
    );
    if (quotesCount[0].count === 1) {
      await query(
        'UPDATE office_orders SET status = $1 WHERE id = $2',
        ['quotes_received', id]
      );
    }

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating quote:', err);
    res.status(500).json({ error: 'Failed to create quote' });
  }
});

// PUT /api/office/orders/:id/quotes/:quoteId - обновить предложение
app.put(`${API_PREFIX}/office/orders/:id/quotes/:quoteId`, async (req, res) => {
  try {
    const { quoteId } = req.params;
    const updates = req.body;

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (updates.isSelected !== undefined) {
      updateFields.push(`is_selected = $${paramIndex++}`);
      values.push(updates.isSelected);
    }
    if (updates.supplierName !== undefined) {
      updateFields.push(`supplier_name = $${paramIndex++}`);
      values.push(updates.supplierName);
    }
    if (updates.totalAmount !== undefined) {
      updateFields.push(`total_amount = $${paramIndex++}`);
      values.push(parseFloat(updates.totalAmount));
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`);
    values.push(quoteId);

    const result = await query(
      `UPDATE office_order_quotes 
       SET ${updateFields.join(', ')}
       WHERE id = $${paramIndex}
       RETURNING *`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Quote not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating quote:', err);
    res.status(500).json({ error: 'Failed to update quote' });
  }
});

// PUT /api/office/orders/:id - обновление заказа
app.put(`${API_PREFIX}/office/orders/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const updates = req.body;

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (updates.status !== undefined) {
      updateFields.push(`status = $${paramIndex++}`);
      values.push(updates.status);
    }
    if (updates.supplierName !== undefined) {
      updateFields.push(`supplier_name = $${paramIndex++}`);
      values.push(updates.supplierName);
    }
    if (updates.supplierContact !== undefined) {
      updateFields.push(`supplier_contact = $${paramIndex++}`);
      values.push(updates.supplierContact);
    }
    if (updates.totalAmount !== undefined) {
      updateFields.push(`total_amount = $${paramIndex++}`);
      values.push(parseFloat(updates.totalAmount));
    }
    if (updates.invoiceId !== undefined) {
      updateFields.push(`invoice_id = $${paramIndex++}`);
      values.push(updates.invoiceId);
    }
    if (updates.invoiceUrl !== undefined) {
      updateFields.push(`invoice_url = $${paramIndex++}`);
      values.push(updates.invoiceUrl);
    }
    if (updates.expectedDate !== undefined) {
      updateFields.push(`expected_date = $${paramIndex++}`);
      values.push(updates.expectedDate);
    }
    if (updates.receivedDate !== undefined) {
      updateFields.push(`received_date = $${paramIndex++}`);
      values.push(updates.receivedDate);
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`);
    values.push(id);

    const result = await query(
      `UPDATE office_orders 
       SET ${updateFields.join(', ')}
       WHERE id = $${paramIndex}
       RETURNING *`,
      values
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Order not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating order:', err);
    res.status(500).json({ error: 'Failed to update order' });
  }
});

// ========= HR: ПРАЗДНИКИ РФ =========

// Праздники РФ (государственные, фиксированные даты). Актуальный список — производственный календарь.
function getRussianHolidaysForYear(year) {
  const y = String(year);
  const list = [
    { date: `${y}-01-01`, name: 'Новый год' },
    { date: `${y}-01-02`, name: 'Новогодние каникулы' },
    { date: `${y}-01-03`, name: 'Новогодние каникулы' },
    { date: `${y}-01-04`, name: 'Новогодние каникулы' },
    { date: `${y}-01-05`, name: 'Новогодние каникулы' },
    { date: `${y}-01-06`, name: 'Новогодние каникулы' },
    { date: `${y}-01-07`, name: 'Рождество Христово' },
    { date: `${y}-01-08`, name: 'Новогодние каникулы' },
    { date: `${y}-02-23`, name: 'День защитника Отечества' },
    { date: `${y}-03-08`, name: 'Международный женский день' },
    { date: `${y}-05-01`, name: 'Праздник Весны и Труда' },
    { date: `${y}-05-09`, name: 'День Победы' },
    { date: `${y}-06-12`, name: 'День России' },
    { date: `${y}-11-04`, name: 'День народного единства' }
  ];
  return list;
}

// GET /api/holidays/ru?year=2025 — праздники РФ на год (для календаря в HR-сводке)
app.get(`${API_PREFIX}/holidays/ru`, async (req, res) => {
  try {
    const year = parseInt(req.query.year, 10) || new Date().getFullYear();
    if (year < 2000 || year > 2100) {
      return res.status(400).json({ error: 'Год должен быть от 2000 до 2100' });
    }
    const holidays = getRussianHolidaysForYear(year);
    res.json({ year, holidays });
  } catch (err) {
    console.error('Error fetching RU holidays:', err);
    res.status(500).json({ error: 'Failed to fetch holidays' });
  }
});

// ========= HR: СВОДКА =========

// GET /api/hr/summary — данные для HR-сводки (KPI, участки, события, частые отпуска/прогулы/больничные)
app.get(`${API_PREFIX}/hr/summary`, async (req, res) => {
  try {
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const yearStart = new Date(today.getFullYear(), 0, 1);
    const in7Days = new Date(today);
    in7Days.setDate(in7Days.getDate() + 7);
    const in14Days = new Date(today);
    in14Days.setDate(in14Days.getDate() + 14);
    const toDate = (d) => d.toISOString().slice(0, 10);
    const todayStr = toDate(today);
    const yearStartStr = toDate(yearStart);
    const in7Str = toDate(in7Days);
    const in14Str = toDate(in14Days);

    const getInitials = (name) => {
      if (!name || typeof name !== 'string') return '??';
      const parts = name.trim().split(/\s+/).filter(Boolean);
      if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
      if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
      return '??';
    };

    // Всего в штате (активные)
    const staffRows = await query(
      `SELECT COUNT(*) AS c FROM employees WHERE status = 'active'`
    );
    const totalStaff = parseInt(staffRows[0]?.c || 0, 10);

    // Вакансии (активные/срочные) и кандидаты
    let vacancyCount = 0;
    let activeCandidates = 0;
    try {
      const vacRows = await query(
        `SELECT COUNT(*) AS c FROM vacancies WHERE status IN ('urgent', 'active')`
      );
      vacancyCount = parseInt(vacRows[0]?.c || 0, 10);
      const candRows = await query(`SELECT COUNT(*) AS c FROM candidates`);
      activeCandidates = parseInt(candRows[0]?.c || 0, 10);
    } catch (e) {
      // таблицы vacancies/candidates могут отсутствовать
    }

    // Текучесть за год: увольнения за последние 12 мес / средняя численность (упрощённо: текущая)
    let turnoverRate = 0;
    try {
      const termRows = await query(
        `SELECT COUNT(*) AS c FROM employee_terminations 
         WHERE termination_date >= $1::date AND termination_date <= $2::date`,
        [toDate(new Date(today.getFullYear() - 1, today.getMonth(), today.getDate() + 1)), todayStr]
      );
      const termCount = parseInt(termRows[0]?.c || 0, 10);
      turnoverRate = totalStaff > 0 ? Math.round((termCount / totalStaff) * 100) : 0;
    } catch (e) {}

    // На обучение (scheduled, in_progress)
    let onTraining = 0;
    try {
      const trRows = await query(
        `SELECT COUNT(DISTINCT employee_id) AS c FROM employee_training 
         WHERE status IN ('scheduled', 'in_progress')`
      );
      onTraining = parseInt(trRows[0]?.c || 0, 10);
    } catch (e) {}

    // Укомплектованность участков (учитываем назначения из employee_districts; один сотрудник может быть на нескольких участках)
    const districts = await query(
      `SELECT id, name FROM districts ORDER BY id`
    );
    let countByDistrict = {};
    try {
      const staffByDistrict = await query(
        `SELECT ed.district_id AS "districtId", COUNT(DISTINCT ed.employee_id) AS c 
         FROM employee_districts ed 
         INNER JOIN employees e ON e.id = ed.employee_id AND e.status = 'active' 
         GROUP BY ed.district_id`
      );
      staffByDistrict.forEach((r) => { countByDistrict[r.districtId] = parseInt(r.c, 10); });
    } catch (_) {
      const staffByDistrict = await query(
        `SELECT assigned_district_id AS "districtId", COUNT(*) AS c 
         FROM employees WHERE status = 'active' AND assigned_district_id IS NOT NULL 
         GROUP BY assigned_district_id`
      );
      staffByDistrict.forEach((r) => { countByDistrict[r.districtId] = parseInt(r.c, 10); });
    }
    const staffingByDistrict = districts.map((d) => ({
      districtId: d.id,
      districtName: d.name,
      current: countByDistrict[d.id] || 0,
      total: countByDistrict[d.id] || 0
    }));

    // События: дни рождения (в ближайшие 14 дней по календарю «в этом году»)
    const allBirthdays = await query(
      `SELECT id, name, birth_date AS "birthDate" FROM employees 
       WHERE status = 'active' AND birth_date IS NOT NULL`
    );
    const thisYear = today.getFullYear();
    const todayTime = today.getTime();
    const in14Time = in14Days.getTime();
    const birthdays = (allBirthdays || [])
      .map((e) => {
        const b = new Date(e.birthDate);
        const thisYearB = new Date(thisYear, b.getMonth(), b.getDate());
        return { ...e, thisYearDate: thisYearB.getTime() };
      })
      .filter((e) => e.thisYearDate >= todayTime && e.thisYearDate <= in14Time)
      .sort((a, b) => a.thisYearDate - b.thisYearDate);
    const birthDateToLabel = (birthDate) => {
      if (!birthDate) return '';
      const b = new Date(birthDate);
      const bM = b.getMonth(), bD = b.getDate();
      const tM = today.getMonth(), tD = today.getDate();
      if (bM === tM && bD === tD) return 'Сегодня';
      const tomorrow = new Date(today);
      tomorrow.setDate(tomorrow.getDate() + 1);
      if (bM === tomorrow.getMonth() && bD === tomorrow.getDate()) return 'Завтра';
      return `День рождения • ${String(bD).padStart(2, '0')}.${String(bM + 1).padStart(2, '0')}`;
    };
    const eventsBirthdays = (birthdays || []).slice(0, 10).map((e) => ({
      employeeId: e.id,
      name: e.name,
      initials: getInitials(e.name),
      date: e.birthDate,
      label: birthDateToLabel(e.birthDate)
    }));

    let eventsVacationReturns = [];
    try {
      const returnsRows = await query(
        `SELECT e.id, e.name, v.end_date AS "endDate"
         FROM employee_vacations v
         JOIN employees e ON e.id = v.employee_id
         WHERE v.status = 'approved' AND v.end_date >= $1::date AND v.end_date <= $2::date
         ORDER BY v.end_date`,
        [todayStr, in7Str]
      );
      eventsVacationReturns = (returnsRows || []).slice(0, 10).map((r) => ({
        employeeId: r.id,
        name: r.name,
        initials: getInitials(r.name),
        date: r.endDate,
        label: `Выход из отпуска • ${String(new Date(r.endDate).getDate()).padStart(2, '0')}.${String(new Date(r.endDate).getMonth() + 1).padStart(2, '0')}`
      }));
    } catch (e) {}

    // Самые частые в отпуске (по количеству записей отпусков)
    let mostFrequentVacation = [];
    try {
      const vacFreq = await query(
        `SELECT e.id, e.name, COUNT(v.id) AS cnt
         FROM employees e
         JOIN employee_vacations v ON v.employee_id = e.id
         WHERE e.status = 'active'
         GROUP BY e.id, e.name ORDER BY cnt DESC LIMIT 10`
      );
      mostFrequentVacation = (vacFreq || []).map((r) => ({
        employeeId: r.id,
        name: r.name,
        initials: getInitials(r.name),
        count: parseInt(r.cnt, 10)
      }));
    } catch (e) {}

    // Самые частые: прогулы, отгулы, опоздания, уходы, больничные (агрегация по сотруднику)
    let mostFrequentAbsences = [];
    try {
      const absAgg = await query(
        `SELECT employee_id AS "employeeId",
         COUNT(*) FILTER (WHERE absence_type = 'day_off') AS "dayOff",
         COUNT(*) FILTER (WHERE absence_type = 'absence') AS "absence",
         COUNT(*) FILTER (WHERE absence_type = 'late') AS "late",
         COUNT(*) FILTER (WHERE absence_type = 'early_leave') AS "earlyLeave"
         FROM employee_absences GROUP BY employee_id`
      );
      const sickAgg = await query(
        `SELECT employee_id AS "employeeId", COUNT(*) AS "sick" FROM employee_sick_leaves GROUP BY employee_id`
      );
      const byEmp = {};
      absAgg.forEach((r) => {
        byEmp[r.employeeId] = {
          dayOff: parseInt(r.dayOff || 0, 10),
          absence: parseInt(r.absence || 0, 10),
          late: parseInt(r.late || 0, 10),
          earlyLeave: parseInt(r.earlyLeave || 0, 10),
          sick: 0
        };
      });
      sickAgg.forEach((r) => {
        if (!byEmp[r.employeeId]) byEmp[r.employeeId] = { dayOff: 0, absence: 0, late: 0, earlyLeave: 0, sick: 0 };
        byEmp[r.employeeId].sick = parseInt(r.sick || 0, 10);
      });
      const empIds = Object.keys(byEmp).filter((id) => {
        const x = byEmp[id];
        return (x.dayOff + x.absence + x.late + x.earlyLeave + x.sick) > 0;
      });
      if (empIds.length > 0) {
        const namesRows = await query(
          `SELECT id, name FROM employees WHERE id = ANY($1::varchar[])`,
          [empIds]
        );
        const names = {};
        namesRows.forEach((r) => { names[r.id] = r.name; });
        mostFrequentAbsences = empIds
          .map((id) => {
            const x = byEmp[id];
            const total = x.dayOff + x.absence + x.late + x.earlyLeave + x.sick;
            return { employeeId: id, name: names[id] || id, initials: getInitials(names[id]), ...x, total };
          })
          .sort((a, b) => b.total - a.total)
          .slice(0, 10);
      }
    } catch (e) {}

    res.json({
      totalStaff,
      vacancyCount,
      activeCandidates,
      turnoverRate,
      onTraining,
      staffingByDistrict,
      events: { birthdays: eventsBirthdays, vacationReturns: eventsVacationReturns },
      mostFrequentVacation,
      mostFrequentAbsences
    });
  } catch (err) {
    console.error('Error fetching HR summary:', err);
    res.status(500).json({ error: 'Failed to fetch HR summary' });
  }
});

// GET /api/hr/template-documents — список типовых документов HR
app.get(`${API_PREFIX}/hr/template-documents`, async (req, res) => {
  try {
    const tableCheck = await query(
      `SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'hr_template_documents')`
    );
    if (!tableCheck[0]?.exists) {
      return res.json([]);
    }
    const rows = await query(
      `SELECT id, name, file_path AS "filePath", original_filename AS "originalFilename", created_at AS "createdAt"
       FROM hr_template_documents ORDER BY created_at DESC`
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching HR template documents:', err);
    res.status(500).json({ error: 'Failed to fetch template documents' });
  }
});

// POST /api/hr/template-documents — загрузка типового документа
app.post(`${API_PREFIX}/hr/template-documents`, uploadHrTemplate.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'Файл не выбран' });
    }
    const name = (req.body && req.body.name && String(req.body.name).trim()) || req.file.originalname || 'Типовой документ';
    const filePath = `hr-templates/${req.file.filename}`;
    const originalFilename = req.file.originalname || null;
    const tableCheck = await query(
      `SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'hr_template_documents')`
    );
    if (!tableCheck[0]?.exists) {
      return res.status(500).json({ error: 'Таблица типовых документов не создана' });
    }
    const result = await query(
      `INSERT INTO hr_template_documents (name, file_path, original_filename)
       VALUES ($1, $2, $3)
       RETURNING id, name, file_path AS "filePath", original_filename AS "originalFilename", created_at AS "createdAt"`,
      [name, filePath, originalFilename]
    );
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error uploading HR template document:', err);
    res.status(500).json({ error: err.message || 'Failed to upload template document' });
  }
});

// GET /api/hr/template-documents/:id/download — скачать типовой документ
app.get(`${API_PREFIX}/hr/template-documents/:id/download`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      `SELECT file_path AS "filePath", original_filename AS "originalFilename", name FROM hr_template_documents WHERE id = $1`,
      [id]
    );
    if (!rows.length) {
      return res.status(404).json({ error: 'Документ не найден' });
    }
    const filePath = path.join(uploadDir, rows[0].filePath);
    if (!fs.existsSync(filePath)) {
      return res.status(404).json({ error: 'Файл не найден на диске' });
    }
    const downloadName = rows[0].originalFilename || rows[0].name || 'document';
    res.download(filePath, downloadName);
  } catch (err) {
    console.error('Error downloading HR template document:', err);
    res.status(500).json({ error: 'Failed to download' });
  }
});

// DELETE /api/hr/template-documents/:id — удаление типового документа (используется в панели управления)
app.delete(`${API_PREFIX}/hr/template-documents/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      `SELECT file_path AS "filePath" FROM hr_template_documents WHERE id = $1`,
      [id]
    );
    if (rows.length) {
      const filePath = path.join(uploadDir, rows[0].filePath);
      if (fs.existsSync(filePath)) {
        fs.unlink(filePath, (err) => {
          if (err) console.error('Ошибка удаления файла типового документа:', err);
        });
      }
    }
    await query('DELETE FROM hr_template_documents WHERE id = $1', [id]);
    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting HR template document:', err);
    res.status(500).json({ error: 'Failed to delete template document' });
  }
});

// ========= HR: ВАКАНСИИ =========

// GET /api/vacancies - получить все вакансии
app.get(`${API_PREFIX}/vacancies`, async (req, res) => {
  try {
    // Проверяем существование таблицы vacancies
    const tableCheck = await query(
      `SELECT EXISTS (
        SELECT FROM information_schema.tables 
        WHERE table_schema = 'public' 
        AND table_name = 'vacancies'
      )`
    );
    
    if (!tableCheck[0]?.exists) {
      return res.status(500).json({ 
        error: 'Failed to fetch vacancies',
        message: 'Table "vacancies" does not exist. Please run database migrations.'
      });
    }

    const { status, department } = req.query;
    let queryText = 'SELECT * FROM vacancies WHERE 1=1';
    const params = [];

    if (status) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }
    if (department) {
      queryText += ` AND department = $${params.length + 1}`;
      params.push(department);
    }

    queryText += ' ORDER BY posted_date DESC, created_at DESC';

    const rows = await query(queryText, params);
    
    // Подсчитываем количество кандидатов для каждой вакансии
    const vacanciesWithCounts = await Promise.all(rows.map(async (vacancy) => {
      try {
        const candidatesResult = await query(
          'SELECT COUNT(*) as count FROM candidates WHERE vacancy_id = $1',
          [vacancy.id]
        );
        return {
          ...vacancy,
          applicantsCount: parseInt(candidatesResult[0]?.count || 0)
        };
      } catch (candidateErr) {
        console.error(`Error counting candidates for vacancy ${vacancy.id}:`, candidateErr);
        return {
          ...vacancy,
          applicantsCount: 0
        };
      }
    }));

    res.json(vacanciesWithCounts);
  } catch (err) {
    console.error('Error fetching vacancies:', err);
    // В режиме разработки возвращаем детальную информацию об ошибке
    const errorMessage = process.env.NODE_ENV === 'production' 
      ? 'Failed to fetch vacancies' 
      : `Failed to fetch vacancies: ${err.message}`;
    res.status(500).json({ 
      error: errorMessage,
      details: process.env.NODE_ENV !== 'production' ? err.stack : undefined
    });
  }
});

// GET /api/vacancies/:id - получить вакансию по ID
app.get(`${API_PREFIX}/vacancies/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('SELECT * FROM vacancies WHERE id = $1', [id]);
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Вакансия не найдена' });
    }

    // Подсчитываем количество кандидатов
    const candidatesResult = await query(
      'SELECT COUNT(*) as count FROM candidates WHERE vacancy_id = $1',
      [id]
    );
    
    const vacancy = {
      ...result[0],
      applicantsCount: parseInt(candidatesResult[0]?.count || 0)
    };

    res.json(vacancy);
  } catch (err) {
    console.error('Error fetching vacancy:', err);
    res.status(500).json({ error: 'Failed to fetch vacancy' });
  }
});

// POST /api/vacancies - создать вакансию
app.post(`${API_PREFIX}/vacancies`, async (req, res) => {
  try {
    const { 
      id, position, department, status, salary, description, 
      requirements, conditions, responsibilities, postedDate, closingDate 
    } = req.body;

    if (!position || !department || !description) {
      return res.status(400).json({ error: 'Позиция, отдел и описание обязательны' });
    }

    // Валидация статуса
    const validStatuses = ['urgent', 'active', 'paused', 'closed'];
    const vacancyStatus = status || 'active';
    if (!validStatuses.includes(vacancyStatus)) {
      return res.status(400).json({ 
        error: `Недопустимый статус: ${vacancyStatus}. Допустимые значения: ${validStatuses.join(', ')}` 
      });
    }

    const vacancyId = id || `vac-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    const posted = postedDate || new Date().toISOString().split('T')[0];

    const result = await query(
      `INSERT INTO vacancies (id, position, department, status, salary, description, requirements, conditions, responsibilities, posted_date, closing_date)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
       RETURNING *`,
      [
        vacancyId, position, department, vacancyStatus, salary || null,
        description, requirements || null, conditions || null, responsibilities || null,
        posted, closingDate || null
      ]
    );

    if (!result || result.length === 0) {
      throw new Error('Запрос выполнен, но не вернул данные');
    }

    const created = result[0];
    try {
      await notificationService.createNotificationForResponsibleZone(pool, 'hr', 'vacancies', {
        type: 'vacancy',
        title: 'Новая вакансия',
        body: `Создана вакансия: ${position} (${department})`,
        entityType: 'vacancy',
        entityId: vacancyId,
      });
    } catch (notifErr) {
      console.warn('Responsibility zone notification (hr/vacancies):', notifErr.message);
    }

    res.status(201).json({ ...created, applicantsCount: 0 });
  } catch (err) {
    console.error('Error creating vacancy:', err);
    console.error('Error details:', {
      message: err.message,
      code: err.code,
      detail: err.detail,
      constraint: err.constraint,
      table: err.table,
      column: err.column
    });
    
    // Отправляем детальную информацию об ошибке
    let errorMessage = 'Failed to create vacancy';
    let statusCode = 500;
    
    if (err.code === '23505') {
      errorMessage = 'Вакансия с таким ID уже существует';
      statusCode = 409;
    } else if (err.code === '23502') {
      errorMessage = `Обязательное поле не заполнено: ${err.column || 'неизвестное поле'}`;
      statusCode = 400;
    } else if (err.code === '23514') {
      errorMessage = `Нарушение ограничения: ${err.constraint || err.message}`;
      statusCode = 400;
    } else if (err.code === '42P01') {
      errorMessage = 'Таблица vacancies не найдена в базе данных';
      statusCode = 500;
    } else if (err.code === '42703') {
      errorMessage = `Ошибка в структуре таблицы: ${err.message}`;
      statusCode = 500;
    } else if (err.message) {
      errorMessage = err.message;
      // Если это ошибка валидации статуса ENUM
      if (err.message.includes('invalid input value for enum')) {
        errorMessage = 'Недопустимое значение статуса. Допустимые: urgent, active, paused, closed';
        statusCode = 400;
      }
    }
    
    res.status(statusCode).json({ 
      error: errorMessage,
      details: process.env.NODE_ENV === 'development' ? {
        message: err.message,
        code: err.code,
        detail: err.detail,
        stack: err.stack
      } : undefined
    });
  }
});

// PUT /api/vacancies/:id - обновить вакансию
app.put(`${API_PREFIX}/vacancies/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { 
      position, department, status, salary, description, 
      requirements, conditions, responsibilities, postedDate, closingDate 
    } = req.body;

    // Проверяем существование вакансии
    const existing = await query('SELECT id FROM vacancies WHERE id = $1', [id]);
    if (existing.length === 0) {
      return res.status(404).json({ error: 'Вакансия не найдена' });
    }

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (position !== undefined) {
      updateFields.push(`position = $${paramIndex++}`);
      values.push(position);
    }
    if (department !== undefined) {
      updateFields.push(`department = $${paramIndex++}`);
      values.push(department);
    }
    if (status !== undefined) {
      updateFields.push(`status = $${paramIndex++}`);
      values.push(status);
    }
    if (salary !== undefined) {
      updateFields.push(`salary = $${paramIndex++}`);
      values.push(salary || null);
    }
    if (description !== undefined) {
      updateFields.push(`description = $${paramIndex++}`);
      values.push(description);
    }
    if (requirements !== undefined) {
      updateFields.push(`requirements = $${paramIndex++}`);
      values.push(requirements || null);
    }
    if (conditions !== undefined) {
      updateFields.push(`conditions = $${paramIndex++}`);
      values.push(conditions || null);
    }
    if (responsibilities !== undefined) {
      updateFields.push(`responsibilities = $${paramIndex++}`);
      values.push(responsibilities || null);
    }
    if (postedDate !== undefined) {
      updateFields.push(`posted_date = $${paramIndex++}`);
      values.push(postedDate);
    }
    if (closingDate !== undefined) {
      updateFields.push(`closing_date = $${paramIndex++}`);
      values.push(closingDate || null);
    }

    if (updateFields.length > 0) {
      updateFields.push(`updated_at = NOW()`);
      values.push(id);
      await query(
        `UPDATE vacancies SET ${updateFields.join(', ')} WHERE id = $${paramIndex}`,
        values
      );
    }

    // Получаем обновленную вакансию
    const result = await query('SELECT * FROM vacancies WHERE id = $1', [id]);
    const candidatesResult = await query(
      'SELECT COUNT(*) as count FROM candidates WHERE vacancy_id = $1',
      [id]
    );
    
    res.json({
      ...result[0],
      applicantsCount: parseInt(candidatesResult[0]?.count || 0)
    });
  } catch (err) {
    console.error('Error updating vacancy:', err);
    res.status(500).json({ error: 'Failed to update vacancy' });
  }
});

// DELETE /api/vacancies/:id - удалить вакансию
app.delete(`${API_PREFIX}/vacancies/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM vacancies WHERE id = $1 RETURNING *', [id]);
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Вакансия не найдена' });
    }

    res.json({ message: 'Вакансия удалена', vacancy: result[0] });
  } catch (err) {
    console.error('Error deleting vacancy:', err);
    res.status(500).json({ error: 'Failed to delete vacancy' });
  }
});

// ========= HR: КАНДИДАТЫ =========

// GET /api/candidates - получить всех кандидатов
app.get(`${API_PREFIX}/candidates`, async (req, res) => {
  try {
    const { vacancyId, stage, includeEvents } = req.query;
    let queryText = 'SELECT * FROM candidates WHERE 1=1';
    const params = [];

    if (vacancyId) {
      queryText += ` AND vacancy_id = $${params.length + 1}`;
      params.push(vacancyId);
    }
    if (stage) {
      queryText += ` AND stage = $${params.length + 1}`;
      params.push(stage);
    }

    queryText += ' ORDER BY created_at DESC';

    const rows = await query(queryText, params);
    
    // Если запрошены события, загружаем их для каждого кандидата
    if (includeEvents === 'true') {
      try {
        for (const candidate of rows) {
          try {
            const events = await query(
              `SELECT 
                id,
                candidate_id AS "candidateId",
                event_type AS "eventType",
                event_date AS "eventDate",
                notes,
                result,
                interviewer,
                location,
                duration_minutes AS "durationMinutes",
                created_at AS "createdAt",
                updated_at AS "updatedAt"
               FROM candidate_events 
               WHERE candidate_id = $1 
               ORDER BY event_date DESC`,
              [candidate.id]
            );
            candidate.events = events;
          } catch (eventError) {
            // Если таблица candidate_events не существует, просто не добавляем события
            if (eventError.code === '42P01') {
              console.warn(`Таблица candidate_events не существует. Миграция будет выполнена автоматически при следующем перезапуске сервера.`);
              candidate.events = [];
            } else {
              throw eventError;
            }
          }
        }
      } catch (err) {
        // Если таблица не существует, просто продолжаем без событий
        if (err.code === '42P01') {
          console.warn(`Таблица candidate_events не существует. Выполните миграцию migrate_candidate_events.sql`);
        } else {
          throw err;
        }
      }
    }
    
    res.json(rows);
  } catch (err) {
    console.error('Error fetching candidates:', err);
    res.status(500).json({ error: 'Failed to fetch candidates' });
  }
});

// POST /api/candidates - создать кандидата
app.post(`${API_PREFIX}/candidates`, async (req, res) => {
  try {
    const { 
      id, name, position, vacancyId, stage, phone, email, 
      resumeUrl, coverLetter, offerSalary, hiredDate
    } = req.body;

    if (!name || !position || !phone) {
      return res.status(400).json({ error: 'Имя, позиция и телефон обязательны' });
    }

    const candidateId = id || `cand-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    const candidateStage = stage || 'new';

    const result = await query(
      `INSERT INTO candidates (id, name, position, vacancy_id, stage, phone, email, resume_url, cover_letter, offer_salary, hired_date)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
       RETURNING *`,
      [
        candidateId, name, position, vacancyId || null, candidateStage,
        phone, email || null, resumeUrl || null, coverLetter || null,
        offerSalary || null, hiredDate || null
      ]
    );

    const createdCandidate = result[0];

    // Если кандидат создается сразу со статусом 'hired', создаем сотрудника
    if (candidateStage === 'hired') {
      const candidateData = {
        name: name,
        position: position,
        phone: phone,
        email: email,
        offerSalary: offerSalary,
        hiredDate: hiredDate || new Date().toISOString().split('T')[0],
        vacancyId: vacancyId
      };

      // Проверяем, не создан ли уже сотрудник с таким именем и телефоном
      const existingEmployee = await query(
        'SELECT id FROM employees WHERE name = $1 AND phone = $2',
        [candidateData.name, candidateData.phone]
      );

      if (existingEmployee.length === 0) {
        // Создаем сотрудника из кандидата
        const employeeId = `e-${Date.now()}`;
        const employeeSalary = candidateData.offerSalary || 0;

        await query(
          `INSERT INTO employees (id, name, position, phone, status, salary, registration_date)
           VALUES ($1, $2, $3, $4, $5, $6, $7)`,
          [
            employeeId,
            candidateData.name,
            candidateData.position,
            candidateData.phone,
            'active',
            employeeSalary,
            candidateData.hiredDate
          ]
        );

        console.log(`Создан сотрудник ${employeeId} из кандидата ${candidateId}`);
      } else {
        console.log(`Сотрудник с именем ${candidateData.name} и телефоном ${candidateData.phone} уже существует`);
      }

      // Закрываем вакансию, если она есть
      if (candidateData.vacancyId) {
        const today = new Date().toISOString().split('T')[0];
        await query(
          `UPDATE vacancies SET status = 'closed', closing_date = $1, updated_at = NOW() WHERE id = $2`,
          [today, candidateData.vacancyId]
        );
        console.log(`Вакансия ${candidateData.vacancyId} закрыта кандидатом ${candidateId}`);
      }
    }

    res.status(201).json(createdCandidate);
  } catch (err) {
    console.error('Error creating candidate:', err);
    res.status(500).json({ error: 'Failed to create candidate' });
  }
});

// PUT /api/candidates/:id - обновить кандидата
app.put(`${API_PREFIX}/candidates/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { 
      name, position, vacancyId, stage, phone, email, resumeUrl, coverLetter,
      interviewDate, interviewNotes, offerSalary, offerDate, hiredDate, rejectedReason
    } = req.body;

    // Получаем текущего кандидата для проверки предыдущего статуса
    const existingCandidate = await query('SELECT * FROM candidates WHERE id = $1', [id]);
    if (existingCandidate.length === 0) {
      return res.status(404).json({ error: 'Кандидат не найден' });
    }

    const currentCandidate = existingCandidate[0];
    const previousStage = currentCandidate.stage;
    const newStage = stage !== undefined ? stage : currentCandidate.stage;

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (name !== undefined) {
      updateFields.push(`name = $${paramIndex++}`);
      values.push(name);
    }
    if (position !== undefined) {
      updateFields.push(`position = $${paramIndex++}`);
      values.push(position);
    }
    if (vacancyId !== undefined) {
      updateFields.push(`vacancy_id = $${paramIndex++}`);
      values.push(vacancyId || null);
    }
    if (stage !== undefined) {
      updateFields.push(`stage = $${paramIndex++}`);
      values.push(stage);
    }
    if (phone !== undefined) {
      updateFields.push(`phone = $${paramIndex++}`);
      values.push(phone);
    }
    if (email !== undefined) {
      updateFields.push(`email = $${paramIndex++}`);
      values.push(email || null);
    }
    if (resumeUrl !== undefined) {
      updateFields.push(`resume_url = $${paramIndex++}`);
      values.push(resumeUrl || null);
    }
    if (coverLetter !== undefined) {
      updateFields.push(`cover_letter = $${paramIndex++}`);
      values.push(coverLetter || null);
    }
    if (interviewDate !== undefined) {
      updateFields.push(`interview_date = $${paramIndex++}`);
      values.push(interviewDate || null);
    }
    if (interviewNotes !== undefined) {
      updateFields.push(`interview_notes = $${paramIndex++}`);
      values.push(interviewNotes || null);
    }
    if (offerSalary !== undefined) {
      updateFields.push(`offer_salary = $${paramIndex++}`);
      values.push(offerSalary || null);
    }
    if (offerDate !== undefined) {
      updateFields.push(`offer_date = $${paramIndex++}`);
      values.push(offerDate || null);
    }
    if (hiredDate !== undefined) {
      updateFields.push(`hired_date = $${paramIndex++}`);
      values.push(hiredDate || null);
    }
    if (rejectedReason !== undefined) {
      updateFields.push(`rejected_reason = $${paramIndex++}`);
      values.push(rejectedReason || null);
    }

    // Если статус меняется на 'hired', автоматически устанавливаем hiredDate если он не указан
    if (newStage === 'hired' && hiredDate === undefined && !currentCandidate.hired_date) {
      updateFields.push(`hired_date = $${paramIndex++}`);
      values.push(new Date().toISOString().split('T')[0]);
    }

    if (updateFields.length > 0) {
      updateFields.push(`updated_at = NOW()`);
      values.push(id);
      await query(
        `UPDATE candidates SET ${updateFields.join(', ')} WHERE id = $${paramIndex}`,
        values
      );
    }

    // Если статус изменился на 'hired', создаем сотрудника и закрываем вакансию
    if (newStage === 'hired' && previousStage !== 'hired') {
      // Получаем обновленные данные кандидата (с учетом установленного hiredDate)
      const updatedCandidate = await query('SELECT * FROM candidates WHERE id = $1', [id]);
      const finalCandidate = updatedCandidate.length > 0 ? updatedCandidate[0] : currentCandidate;
      
      const candidateData = {
        name: name !== undefined ? name : finalCandidate.name,
        position: position !== undefined ? position : finalCandidate.position,
        phone: phone !== undefined ? phone : finalCandidate.phone,
        email: email !== undefined ? email : finalCandidate.email,
        offerSalary: offerSalary !== undefined ? offerSalary : finalCandidate.offer_salary,
        hiredDate: finalCandidate.hired_date || new Date().toISOString().split('T')[0],
        vacancyId: vacancyId !== undefined ? vacancyId : finalCandidate.vacancy_id
      };

      // Проверяем, не создан ли уже сотрудник с таким именем и телефоном
      const existingEmployee = await query(
        'SELECT id FROM employees WHERE name = $1 AND phone = $2',
        [candidateData.name, candidateData.phone]
      );

      if (existingEmployee.length === 0) {
        // Создаем сотрудника из кандидата
        const employeeId = `e-${Date.now()}`;
        const employeeSalary = candidateData.offerSalary || 0; // Используем предложенную зарплату или 0

        await query(
          `INSERT INTO employees (id, name, position, phone, status, salary, registration_date)
           VALUES ($1, $2, $3, $4, $5, $6, $7)`,
          [
            employeeId,
            candidateData.name,
            candidateData.position,
            candidateData.phone,
            'active',
            employeeSalary,
            candidateData.hiredDate
          ]
        );

        console.log(`Создан сотрудник ${employeeId} из кандидата ${id}`);
      } else {
        console.log(`Сотрудник с именем ${candidateData.name} и телефоном ${candidateData.phone} уже существует`);
      }

      // Закрываем вакансию, если она есть
      if (candidateData.vacancyId) {
        const today = new Date().toISOString().split('T')[0];
        await query(
          `UPDATE vacancies SET status = 'closed', closing_date = $1, updated_at = NOW() WHERE id = $2`,
          [today, candidateData.vacancyId]
        );
        console.log(`Вакансия ${candidateData.vacancyId} закрыта кандидатом ${id}`);
      }
    }

    // Получаем обновленного кандидата
    const result = await query('SELECT * FROM candidates WHERE id = $1', [id]);
    res.json(result[0]);
  } catch (err) {
    console.error('Error updating candidate:', err);
    res.status(500).json({ error: 'Failed to update candidate' });
  }
});

// DELETE /api/candidates/:id - удалить кандидата
app.delete(`${API_PREFIX}/candidates/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM candidates WHERE id = $1 RETURNING *', [id]);
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Кандидат не найден' });
    }

    res.json({ message: 'Кандидат удален', candidate: result[0] });
  } catch (err) {
    console.error('Error deleting candidate:', err);
    res.status(500).json({ error: 'Failed to delete candidate' });
  }
});

// GET /api/vacancies/:id/candidates - получить кандидатов по вакансии
app.get(`${API_PREFIX}/vacancies/:id/candidates`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query(
      'SELECT * FROM candidates WHERE vacancy_id = $1 ORDER BY created_at DESC',
      [id]
    );
    res.json(result);
  } catch (err) {
    console.error('Error fetching candidates for vacancy:', err);
    res.status(500).json({ error: 'Failed to fetch candidates' });
  }
});

// ========= HR: СОБЫТИЯ КАНДИДАТОВ =========
// ВАЖНО: Эти роуты должны быть зарегистрированы ПЕРЕД общим роутом /candidates/:id
// чтобы Express правильно их обрабатывал

// GET /api/candidates/:id/events - получить все события кандидата
app.get(`${API_PREFIX}/candidates/:id/events`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query(
      `SELECT 
        id,
        candidate_id AS "candidateId",
        event_type AS "eventType",
        event_date AS "eventDate",
        notes,
        result,
        interviewer,
        location,
        duration_minutes AS "durationMinutes",
        created_at AS "createdAt",
        updated_at AS "updatedAt"
       FROM candidate_events 
       WHERE candidate_id = $1 
       ORDER BY event_date DESC`,
      [id]
    );
    res.json(result);
  } catch (err) {
    console.error('Error fetching candidate events:', err);
    if (err.code === '42P01') {
      return res.status(503).json({ 
        error: 'Таблица candidate_events не существует. Перезапустите сервер - миграция выполнится автоматически, или выполните migrate_candidate_events.sql вручную' 
      });
    }
    res.status(500).json({ error: 'Failed to fetch candidate events' });
  }
});

// POST /api/candidates/:id/events - создать событие для кандидата
app.post(`${API_PREFIX}/candidates/:id/events`, async (req, res) => {
  try {
    const { id } = req.params;
    const { 
      eventType, eventDate, notes, result, interviewer, location, durationMinutes 
    } = req.body;

    if (!eventType || !eventDate) {
      return res.status(400).json({ error: 'Тип события и дата обязательны' });
    }

    // Проверяем существование кандидата и получаем его данные
    const candidate = await query('SELECT * FROM candidates WHERE id = $1', [id]);
    if (candidate.length === 0) {
      return res.status(404).json({ error: 'Кандидат не найден' });
    }

    const candidateData = candidate[0];

    const eventId = `evt-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

    // Определяем новый статус кандидата на основе типа события
    let newStage = candidateData.stage; // По умолчанию оставляем текущий статус
    let updateStage = false;

    switch (eventType) {
      case 'call':
        // Созвон не меняет статус, но может быть первым шагом
        if (candidateData.stage === 'new') {
          // Можно оставить 'new' или перейти к следующему этапу
        }
        break;
      case 'interview_1':
      case 'interview_2':
      case 'interview_3':
        newStage = 'interview';
        updateStage = true;
        break;
      case 'test_task':
        // Тестовое задание обычно после собеседования
        if (candidateData.stage === 'interview') {
          // Оставляем 'interview'
        }
        break;
      case 'offer':
        // Оффер - можно оставить текущий статус или перейти к 'probation'
        break;
      case 'offer_accepted':
        newStage = 'probation';
        updateStage = true;
        break;
      case 'offer_rejected':
        newStage = 'rejected';
        updateStage = true;
        break;
      case 'probation_start':
        newStage = 'probation';
        updateStage = true;
        break;
      case 'hired':
        newStage = 'hired';
        updateStage = true;
        break;
      case 'rejected':
        newStage = 'rejected';
        updateStage = true;
        break;
    }

    // Обновляем статус кандидата если нужно
    if (updateStage && newStage !== candidateData.stage) {
      await query(
        `UPDATE candidates SET stage = $1, updated_at = NOW() WHERE id = $2`,
        [newStage, id]
      );
      console.log(`Статус кандидата ${id} изменен с '${candidateData.stage}' на '${newStage}' через событие '${eventType}'`);
    }

    // Создаем событие
    let result_query;
    try {
      result_query = await query(
        `INSERT INTO candidate_events (id, candidate_id, event_type, event_date, notes, result, interviewer, location, duration_minutes)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
         RETURNING 
           id,
           candidate_id AS "candidateId",
           event_type AS "eventType",
           event_date AS "eventDate",
           notes,
           result,
           interviewer,
           location,
           duration_minutes AS "durationMinutes",
           created_at AS "createdAt",
           updated_at AS "updatedAt"`,
        [
          eventId, id, eventType, eventDate, notes || null, result || 'pending',
          interviewer || null, location || null, durationMinutes || null
        ]
      );
    } catch (dbError) {
      console.error('Database error creating event:', dbError);
      // Проверяем, может быть проблема с типами ENUM
      if (dbError.message && dbError.message.includes('invalid input value for enum')) {
        return res.status(400).json({ 
          error: `Неверный тип события: ${eventType}. Доступные типы: call, interview_1, interview_2, interview_3, test_task, offer, offer_accepted, offer_rejected, probation_start, hired, rejected, other` 
        });
      }
      // Проверяем, существует ли таблица
      if (dbError.code === '42P01') {
        return res.status(503).json({ 
          error: 'Таблица candidate_events не существует. Перезапустите сервер - миграция выполнится автоматически, или выполните migrate_candidate_events.sql вручную' 
        });
      }
      throw dbError;
    }

    // Если создается событие типа 'hired', создаем сотрудника
    if (eventType === 'hired') {
      // Обновляем статус кандидата на 'hired' и устанавливаем hiredDate
      const hiredDateValue = new Date(eventDate).toISOString().split('T')[0];
      await query(
        `UPDATE candidates SET stage = 'hired', hired_date = $1, updated_at = NOW() WHERE id = $2`,
        [hiredDateValue, id]
      );

      // Создаем сотрудника из кандидата
      const candidateEmployeeData = {
        name: candidateData.name,
        position: candidateData.position,
        phone: candidateData.phone,
        email: candidateData.email,
        offerSalary: candidateData.offer_salary,
        hiredDate: hiredDateValue,
        vacancyId: candidateData.vacancy_id
      };

      // Проверяем, не создан ли уже сотрудник с таким именем и телефоном
      const existingEmployee = await query(
        'SELECT id FROM employees WHERE name = $1 AND phone = $2',
        [candidateEmployeeData.name, candidateEmployeeData.phone]
      );

      if (existingEmployee.length === 0) {
        // Создаем сотрудника из кандидата
        const employeeId = `e-${Date.now()}`;
        const employeeSalary = candidateEmployeeData.offerSalary || 0;

        await query(
          `INSERT INTO employees (id, name, position, phone, status, salary, registration_date)
           VALUES ($1, $2, $3, $4, $5, $6, $7)`,
          [
            employeeId,
            candidateEmployeeData.name,
            candidateEmployeeData.position,
            candidateEmployeeData.phone,
            'active',
            employeeSalary,
            candidateEmployeeData.hiredDate
          ]
        );

        console.log(`Создан сотрудник ${employeeId} из кандидата ${id} через событие 'hired'`);
      } else {
        console.log(`Сотрудник с именем ${candidateEmployeeData.name} и телефоном ${candidateEmployeeData.phone} уже существует`);
      }

      // Закрываем вакансию, если она есть
      if (candidateEmployeeData.vacancyId) {
        const today = new Date().toISOString().split('T')[0];
        await query(
          `UPDATE vacancies SET status = 'closed', closing_date = $1, updated_at = NOW() WHERE id = $2`,
          [today, candidateEmployeeData.vacancyId]
        );
        console.log(`Вакансия ${candidateEmployeeData.vacancyId} закрыта кандидатом ${id}`);
      }
    }

    res.status(201).json(result_query[0]);
  } catch (err) {
    console.error('Error creating candidate event:', err);
    const errorMessage = err.message || 'Failed to create candidate event';
    res.status(500).json({ 
      error: 'Failed to create candidate event',
      details: errorMessage 
    });
  }
});

// PUT /api/candidates/:id/events/:eventId - обновить событие кандидата
app.put(`${API_PREFIX}/candidates/:id/events/:eventId`, async (req, res) => {
  try {
    const { id, eventId } = req.params;
    const { 
      eventType, eventDate, notes, result, interviewer, location, durationMinutes 
    } = req.body;

    // Проверяем существование события
    let existing;
    try {
      existing = await query(
        'SELECT id FROM candidate_events WHERE id = $1 AND candidate_id = $2',
        [eventId, id]
      );
    } catch (dbError) {
      if (dbError.code === '42P01') {
        return res.status(503).json({ 
          error: 'Таблица candidate_events не существует. Выполните миграцию migrate_candidate_events.sql' 
        });
      }
      throw dbError;
    }
    
    if (existing.length === 0) {
      return res.status(404).json({ error: 'Событие не найдено' });
    }

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (eventType !== undefined) {
      updateFields.push(`event_type = $${paramIndex++}`);
      values.push(eventType);
    }
    if (eventDate !== undefined) {
      updateFields.push(`event_date = $${paramIndex++}`);
      values.push(eventDate);
    }
    if (notes !== undefined) {
      updateFields.push(`notes = $${paramIndex++}`);
      values.push(notes || null);
    }
    if (result !== undefined) {
      updateFields.push(`result = $${paramIndex++}`);
      values.push(result);
    }
    if (interviewer !== undefined) {
      updateFields.push(`interviewer = $${paramIndex++}`);
      values.push(interviewer || null);
    }
    if (location !== undefined) {
      updateFields.push(`location = $${paramIndex++}`);
      values.push(location || null);
    }
    if (durationMinutes !== undefined) {
      updateFields.push(`duration_minutes = $${paramIndex++}`);
      values.push(durationMinutes || null);
    }

    if (updateFields.length > 0) {
      updateFields.push(`updated_at = NOW()`);
      values.push(eventId);
      try {
        await query(
          `UPDATE candidate_events SET ${updateFields.join(', ')} WHERE id = $${paramIndex}`,
          values
        );
      } catch (dbError) {
        if (dbError.code === '42P01') {
          return res.status(503).json({ 
            error: 'Таблица candidate_events не существует. Выполните миграцию migrate_candidate_events.sql' 
          });
        }
        throw dbError;
      }
    }

    let result_query;
    try {
      result_query = await query(
        `SELECT 
          id,
          candidate_id AS "candidateId",
          event_type AS "eventType",
          event_date AS "eventDate",
          notes,
          result,
          interviewer,
          location,
          duration_minutes AS "durationMinutes",
          created_at AS "createdAt",
          updated_at AS "updatedAt"
         FROM candidate_events 
         WHERE id = $1`,
        [eventId]
      );
    } catch (dbError) {
      if (dbError.code === '42P01') {
        return res.status(503).json({ 
          error: 'Таблица candidate_events не существует. Перезапустите сервер - миграция выполнится автоматически, или выполните migrate_candidate_events.sql вручную' 
        });
      }
      throw dbError;
    }
    res.json(result_query[0]);
  } catch (err) {
    console.error('Error updating candidate event:', err);
    res.status(500).json({ error: 'Failed to update candidate event' });
  }
});

// DELETE /api/candidates/:id/events/:eventId - удалить событие кандидата
app.delete(`${API_PREFIX}/candidates/:id/events/:eventId`, async (req, res) => {
  try {
    const { id, eventId } = req.params;
    let result;
    try {
      result = await query(
        `DELETE FROM candidate_events 
         WHERE id = $1 AND candidate_id = $2 
         RETURNING 
           id,
           candidate_id AS "candidateId",
           event_type AS "eventType",
           event_date AS "eventDate",
           notes,
           result,
           interviewer,
           location,
           duration_minutes AS "durationMinutes",
           created_at AS "createdAt",
           updated_at AS "updatedAt"`,
        [eventId, id]
      );
    } catch (dbError) {
      if (dbError.code === '42P01') {
        return res.status(503).json({ 
          error: 'Таблица candidate_events не существует. Выполните миграцию migrate_candidate_events.sql' 
        });
      }
      throw dbError;
    }
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Событие не найдено' });
    }

    res.json({ message: 'Событие удалено', event: result[0] });
  } catch (err) {
    console.error('Error deleting candidate event:', err);
    res.status(500).json({ error: 'Failed to delete candidate event' });
  }
});

// Обновляем GET /api/candidates/:id чтобы возвращать события
app.get(`${API_PREFIX}/candidates/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('SELECT * FROM candidates WHERE id = $1', [id]);
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Кандидат не найден' });
    }

    // Получаем события кандидата
    let events = [];
    try {
      const eventsResult = await query(
        `SELECT 
          id,
          candidate_id AS "candidateId",
          event_type AS "eventType",
          event_date AS "eventDate",
          notes,
          result,
          interviewer,
          location,
          duration_minutes AS "durationMinutes",
          created_at AS "createdAt",
          updated_at AS "updatedAt"
         FROM candidate_events 
         WHERE candidate_id = $1 
         ORDER BY event_date DESC`,
        [id]
      );
      events = eventsResult;
    } catch (eventError) {
      // Если таблица candidate_events не существует, просто возвращаем пустой массив
      if (eventError.code === '42P01') {
        console.warn(`Таблица candidate_events не существует. Миграция будет выполнена автоматически при следующем перезапуске сервера.`);
        events = [];
      } else {
        throw eventError;
      }
    }

    res.json({
      ...result[0],
      events: events
    });
  } catch (err) {
    console.error('Error fetching candidate:', err);
    res.status(500).json({ error: 'Failed to fetch candidate' });
  }
});

// Дублирующий роут удален - он уже определен выше (строка 3707)

// ========= HR: ИНСТРУКТАЖИ И КУРСЫ (БЛОК ОБУЧЕНИЯ) =========

// Функция нормализации полей программы обучения (snake_case -> camelCase)
function normalizeTrainingProgram(program) {
  if (!program) return null;
  return {
    id: program.id,
    title: program.title,
    description: program.description,
    type: program.type,
    category: program.category,
    durationHours: program.duration_hours || program.durationHours,
    validityMonths: program.validity_months || program.validityMonths,
    isRequired: program.is_required !== undefined ? program.is_required : program.isRequired,
    requiredForPositions: program.required_for_positions || program.requiredForPositions,
    instructorName: program.instructor_name || program.instructorName,
    materialsUrl: program.materials_url || program.materialsUrl,
    createdAt: program.created_at || program.createdAt,
    updatedAt: program.updated_at || program.updatedAt
  };
}

// GET /api/training/programs - получить все программы обучения
app.get(`${API_PREFIX}/training/programs`, async (req, res) => {
  try {
    // Проверяем существование таблицы
    const tableCheck = await query(
      `SELECT table_name 
       FROM information_schema.tables 
       WHERE table_schema = 'public' 
       AND table_name = 'training_programs'`
    );

    if (!tableCheck || tableCheck.length === 0) {
      return res.json([]); // Таблица еще не создана - возвращаем пустой массив
    }

    const { type, category, isRequired } = req.query;
    let queryText = 'SELECT * FROM training_programs WHERE 1=1';
    const params = [];

    if (type) {
      queryText += ` AND type = $${params.length + 1}`;
      params.push(type);
    }
    if (category) {
      queryText += ` AND category = $${params.length + 1}`;
      params.push(category);
    }
    if (isRequired === 'true') {
      queryText += ` AND is_required = true`;
    }

    queryText += ' ORDER BY created_at DESC';

    const result = await query(queryText, params);
    const normalized = (result || []).map(normalizeTrainingProgram);
    res.json(normalized);
  } catch (err) {
    console.error('Error fetching training programs:', err);
    // Если таблица не существует, возвращаем пустой массив
    if (err.code === '42P01') {
      return res.json([]);
    }
    res.status(500).json({ error: 'Failed to fetch training programs' });
  }
});

// GET /api/training/programs/:id - получить программу обучения
app.get(`${API_PREFIX}/training/programs/:id`, async (req, res) => {
  try {
    // Проверяем существование таблицы
    const tableCheck = await query(
      `SELECT table_name 
       FROM information_schema.tables 
       WHERE table_schema = 'public' 
       AND table_name = 'training_programs'`
    );

    if (!tableCheck || tableCheck.length === 0) {
      return res.status(404).json({ error: 'Программа обучения не найдена' });
    }

    const { id } = req.params;
    const result = await query('SELECT * FROM training_programs WHERE id = $1', [id]);
    
    if (!result || result.length === 0) {
      return res.status(404).json({ 
        error: 'Программа обучения не найдена',
        details: `Программа с ID "${id}" не существует`
      });
    }

    res.json(normalizeTrainingProgram(result[0]));
  } catch (err) {
    console.error('Error fetching training program:', err);
    if (err.code === '42P01') {
      return res.status(404).json({ error: 'Программа обучения не найдена' });
    }
    res.status(500).json({ error: 'Failed to fetch training program' });
  }
});

// POST /api/training/programs - создать программу обучения
app.post(`${API_PREFIX}/training/programs`, async (req, res) => {
  try {
    // Проверяем существование таблицы
    const tableCheck = await query(
      `SELECT table_name 
       FROM information_schema.tables 
       WHERE table_schema = 'public' 
       AND table_name = 'training_programs'`
    );

    if (!tableCheck || tableCheck.length === 0) {
      return res.status(503).json({ 
        error: 'Таблица training_programs не существует. Перезапустите сервер - миграция выполнится автоматически' 
      });
    }

    const {
      id, title, description, type, category, durationHours, validityMonths,
      isRequired, requiredForPositions, instructorName, materialsUrl
    } = req.body;

    if (!title || !type || !category) {
      return res.status(400).json({ error: 'Название, тип и категория обязательны' });
    }

    // При создании новой программы id генерируется автоматически, если не передан
    // Если id передан, используем его (для случаев, когда нужно задать конкретный id)
    const programId = id || `training-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

    // Преобразуем requiredForPositions в массив, если это строка или массив
    let positionsArray = null;
    if (requiredForPositions) {
      if (Array.isArray(requiredForPositions)) {
        positionsArray = requiredForPositions;
      } else if (typeof requiredForPositions === 'string') {
        // Если строка не пустая, разбиваем по запятой
        const trimmed = requiredForPositions.trim();
        if (trimmed) {
          positionsArray = trimmed.split(',').map(p => p.trim()).filter(p => p);
        }
      }
    }

    const result = await query(
      `INSERT INTO training_programs (
        id, title, description, type, category, duration_hours, validity_months,
        is_required, required_for_positions, instructor_name, materials_url
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
      RETURNING *`,
      [
        programId, title, description || null, type, category,
        durationHours || null, validityMonths || null,
        isRequired || false, positionsArray || null,
        instructorName || null, materialsUrl || null
      ]
    );

    if (!result || result.length === 0) {
      return res.status(500).json({ error: 'Не удалось создать программу обучения' });
    }

    const normalized = normalizeTrainingProgram(result[0]);
    console.log('[Training] Created program:', { id: normalized.id, title: normalized.title });
    res.status(201).json(normalized);
  } catch (err) {
    console.error('Error creating training program:', err);
    
    // Проверяем тип ошибки
    if (err.code === '42P01') {
      return res.status(503).json({ 
        error: 'Таблица training_programs не существует. Перезапустите сервер - миграция выполнится автоматически' 
      });
    }
    
    if (err.code === '42P02' || err.message?.includes('invalid input value for enum')) {
      return res.status(400).json({ 
        error: `Неверное значение типа или категории. Доступные типы: instruction, course, certification, exam, other. Категории: safety, fire_safety, electrical, first_aid, professional, compliance, other`,
        details: err.message
      });
    }

    res.status(500).json({ 
      error: 'Failed to create training program',
      details: err.message 
    });
  }
});

// PUT /api/training/programs/:id - обновить программу обучения
app.put(`${API_PREFIX}/training/programs/:id`, async (req, res) => {
  try {
    // Проверяем существование таблицы
    const tableCheck = await query(
      `SELECT table_name 
       FROM information_schema.tables 
       WHERE table_schema = 'public' 
       AND table_name = 'training_programs'`
    );

    if (!tableCheck || tableCheck.length === 0) {
      return res.status(503).json({ 
        error: 'Таблица training_programs не существует. Перезапустите сервер - миграция выполнится автоматически' 
      });
    }

    const { id } = req.params;
    const {
      title, description, type, category, durationHours, validityMonths,
      isRequired, requiredForPositions, instructorName, materialsUrl
    } = req.body;

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (title !== undefined) {
      updateFields.push(`title = $${paramIndex++}`);
      values.push(title);
    }
    if (description !== undefined) {
      updateFields.push(`description = $${paramIndex++}`);
      values.push(description || null);
    }
    if (type !== undefined) {
      updateFields.push(`type = $${paramIndex++}`);
      values.push(type);
    }
    if (category !== undefined) {
      updateFields.push(`category = $${paramIndex++}`);
      values.push(category);
    }
    if (durationHours !== undefined) {
      updateFields.push(`duration_hours = $${paramIndex++}`);
      values.push(durationHours || null);
    }
    if (validityMonths !== undefined) {
      updateFields.push(`validity_months = $${paramIndex++}`);
      values.push(validityMonths || null);
    }
    if (isRequired !== undefined) {
      updateFields.push(`is_required = $${paramIndex++}`);
      values.push(isRequired);
    }
    if (requiredForPositions !== undefined) {
      // Преобразуем requiredForPositions в массив, если это строка или массив
      let positionsArray = null;
      if (Array.isArray(requiredForPositions)) {
        positionsArray = requiredForPositions;
      } else if (typeof requiredForPositions === 'string') {
        const trimmed = requiredForPositions.trim();
        if (trimmed) {
          positionsArray = trimmed.split(',').map(p => p.trim()).filter(p => p);
        }
      }
      updateFields.push(`required_for_positions = $${paramIndex++}`);
      values.push(positionsArray || null);
    }
    if (instructorName !== undefined) {
      updateFields.push(`instructor_name = $${paramIndex++}`);
      values.push(instructorName || null);
    }
    if (materialsUrl !== undefined) {
      updateFields.push(`materials_url = $${paramIndex++}`);
      values.push(materialsUrl || null);
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`);
    values.push(id);

    const updateResult = await query(
      `UPDATE training_programs SET ${updateFields.join(', ')} WHERE id = $${paramIndex}`,
      values
    );

    const result = await query('SELECT * FROM training_programs WHERE id = $1', [id]);
    
    if (!result || result.length === 0) {
      console.error(`[Training] Program not found for update: ${id}`);
      return res.status(404).json({ 
        error: 'Программа обучения не найдена',
        details: `Программа с ID "${id}" не существует в базе данных`
      });
    }
    
    const normalized = normalizeTrainingProgram(result[0]);
    console.log('[Training] Updated program:', { id: normalized.id, title: normalized.title });
    res.json(normalized);
  } catch (err) {
    console.error('Error updating training program:', err);
    
    if (err.code === '42P01') {
      return res.status(503).json({ 
        error: 'Таблица training_programs не существует. Перезапустите сервер - миграция выполнится автоматически' 
      });
    }
    
    if (err.code === '42P02' || err.message?.includes('invalid input value for enum')) {
      return res.status(400).json({ 
        error: `Неверное значение типа или категории`,
        details: err.message
      });
    }

    res.status(500).json({ 
      error: 'Failed to update training program',
      details: err.message 
    });
  }
});

// DELETE /api/training/programs/:id - удалить программу обучения
app.delete(`${API_PREFIX}/training/programs/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM training_programs WHERE id = $1 RETURNING *', [id]);
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Программа обучения не найдена' });
    }

    res.json({ message: 'Программа обучения удалена', program: result[0] });
  } catch (err) {
    console.error('Error deleting training program:', err);
    res.status(500).json({ error: 'Failed to delete training program' });
  }
});

// GET /api/training/employee/:employeeId - получить обучение сотрудника
app.get(`${API_PREFIX}/training/employee/:employeeId`, async (req, res) => {
  try {
    // Проверяем существование таблиц
    const tableCheck = await query(
      `SELECT table_name 
       FROM information_schema.tables 
       WHERE table_schema = 'public' 
       AND table_name = 'employee_training'`
    );

    if (!tableCheck || tableCheck.length === 0) {
      return res.json([]); // Таблица еще не создана - возвращаем пустой массив
    }

    const { employeeId } = req.params;
    const { status, expired } = req.query;

    let queryText = `
      SELECT 
        et.id,
        et.employee_id AS "employeeId",
        et.program_id AS "programId",
        et.status,
        et.start_date AS "startDate",
        et.completion_date AS "completionDate",
        et.expiry_date AS "expiryDate",
        et.score,
        et.passed,
        et.certificate_number AS "certificateNumber",
        et.certificate_url AS "certificateUrl",
        et.notes,
        et.instructor_name AS "instructorName",
        et.location,
        et.created_at AS "createdAt",
        et.updated_at AS "updatedAt",
        e.name AS employee_name,
        e.position AS employee_position,
        tp.title AS program_title,
        tp.type AS program_type,
        tp.category AS program_category,
        tp.duration_hours AS program_duration_hours,
        tp.validity_months AS program_validity_months
      FROM employee_training et
      INNER JOIN employees e ON et.employee_id = e.id
      INNER JOIN training_programs tp ON et.program_id = tp.id
      WHERE et.employee_id = $1
    `;
    const params = [employeeId];

    if (status) {
      queryText += ` AND et.status = $${params.length + 1}`;
      params.push(status);
    }
    if (expired === 'true') {
      queryText += ` AND et.expiry_date < CURRENT_DATE`;
    }

    queryText += ' ORDER BY et.created_at DESC';

    const result = await query(queryText, params);
    
    // Нормализуем поля в camelCase
    const normalized = (result || []).map(row => ({
      id: row.id,
      employeeId: row.employeeId,
      programId: row.programId,
      status: row.status,
      startDate: row.startDate,
      completionDate: row.completionDate,
      expiryDate: row.expiryDate,
      score: row.score,
      passed: row.passed,
      certificateNumber: row.certificateNumber,
      certificateUrl: row.certificateUrl,
      notes: row.notes,
      instructorName: row.instructorName,
      location: row.location,
      createdAt: row.createdAt,
      updatedAt: row.updatedAt,
      employeeName: row.employee_name,
      employeePosition: row.employee_position,
      programTitle: row.program_title,
      programType: row.program_type,
      programCategory: row.program_category,
      programDurationHours: row.program_duration_hours,
      programValidityMonths: row.program_validity_months
    }));
    
    res.json(normalized);
  } catch (err) {
    console.error('Error fetching employee training:', err);
    // Если таблица не существует, возвращаем пустой массив
    if (err.code === '42P01') {
      return res.json([]);
    }
    res.status(500).json({ error: 'Failed to fetch employee training' });
  }
});

// POST /api/training/employee/:employeeId - назначить обучение сотруднику
app.post(`${API_PREFIX}/training/employee/:employeeId`, async (req, res) => {
  try {
    const { employeeId } = req.params;
    const {
      programId, startDate, instructorName, location, notes
    } = req.body;

    if (!programId) {
      return res.status(400).json({ error: 'ID программы обучения обязателен' });
    }

    // Проверяем существование таблицы
    const tableCheck = await query(
      `SELECT table_name 
       FROM information_schema.tables 
       WHERE table_schema = 'public' 
       AND table_name = 'training_programs'`
    );

    if (!tableCheck || tableCheck.length === 0) {
      return res.status(503).json({ 
        error: 'Таблица training_programs не существует. Перезапустите сервер - миграция выполнится автоматически' 
      });
    }

    // Проверяем существование программы
    const program = await query('SELECT * FROM training_programs WHERE id = $1', [programId]);
    if (!program || program.length === 0) {
      console.error(`[Training] Program not found: ${programId}`);
      // Попробуем найти все программы для отладки
      const allPrograms = await query('SELECT id, title FROM training_programs LIMIT 10');
      console.log('[Training] Available programs:', allPrograms.map(p => ({ id: p.id, title: p.title })));
      return res.status(404).json({ 
        error: 'Программа обучения не найдена',
        details: `Программа с ID "${programId}" не существует в базе данных`
      });
    }
    
    console.log(`[Training] Assigning program ${programId} to employee ${employeeId}`);

    // Рассчитываем дату окончания срока действия
    let expiryDate = null;
    if (program[0].validity_months && startDate) {
      const start = new Date(startDate);
      start.setMonth(start.getMonth() + program[0].validity_months);
      expiryDate = start.toISOString().split('T')[0];
    }

    // Проверяем, не назначено ли уже это обучение
    const existing = await query(
      'SELECT id FROM employee_training WHERE employee_id = $1 AND program_id = $2',
      [employeeId, programId]
    );

    let result;
    if (existing.length > 0) {
      // Обновляем существующую запись
      await query(
        `UPDATE employee_training SET
          start_date = $1,
          instructor_name = $2,
          location = $3,
          notes = $4,
          expiry_date = $5,
          status = 'in_progress',
          updated_at = NOW()
        WHERE id = $6`,
        [
          startDate || null,
          instructorName || null,
          location || null,
          notes || null,
          expiryDate,
          existing[0].id
        ]
      );
      result = await query('SELECT * FROM employee_training WHERE id = $1', [existing[0].id]);
    } else {
      // Создаем новую запись
      result = await query(
        `INSERT INTO employee_training (
          employee_id, program_id, status, start_date, instructor_name, location, notes, expiry_date
        ) VALUES ($1, $2, 'in_progress', $3, $4, $5, $6, $7)
        RETURNING *`,
        [
          employeeId, programId, startDate || null,
          instructorName || null, location || null, notes || null, expiryDate
        ]
      );
    }

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error assigning training:', err);
    res.status(500).json({ error: 'Failed to assign training' });
  }
});

// PUT /api/training/employee/:employeeId/:trainingId - обновить статус обучения
app.put(`${API_PREFIX}/training/employee/:employeeId/:trainingId`, async (req, res) => {
  try {
    const { employeeId, trainingId } = req.params;
    const {
      status, completionDate, score, passed, certificateNumber, certificateUrl, notes
    } = req.body;

    // Проверяем существование обучения
    const existing = await query(
      'SELECT * FROM employee_training WHERE id = $1 AND employee_id = $2',
      [trainingId, employeeId]
    );

    if (existing.length === 0) {
      return res.status(404).json({ error: 'Обучение не найдено' });
    }

    const updateFields = [];
    const values = [];
    let paramIndex = 1;

    if (status !== undefined) {
      updateFields.push(`status = $${paramIndex++}`);
      values.push(status);
    }
    if (completionDate !== undefined) {
      updateFields.push(`completion_date = $${paramIndex++}`);
      values.push(completionDate || null);
    }
    if (score !== undefined) {
      updateFields.push(`score = $${paramIndex++}`);
      values.push(score || null);
    }
    if (passed !== undefined) {
      updateFields.push(`passed = $${paramIndex++}`);
      values.push(passed);
    }
    if (certificateNumber !== undefined) {
      updateFields.push(`certificate_number = $${paramIndex++}`);
      values.push(certificateNumber || null);
    }
    if (certificateUrl !== undefined) {
      updateFields.push(`certificate_url = $${paramIndex++}`);
      values.push(certificateUrl || null);
    }
    if (notes !== undefined) {
      updateFields.push(`notes = $${paramIndex++}`);
      values.push(notes || null);
    }

    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'Нет полей для обновления' });
    }

    updateFields.push(`updated_at = NOW()`);
    values.push(trainingId);

    await query(
      `UPDATE employee_training SET ${updateFields.join(', ')} WHERE id = $${paramIndex}`,
      values
    );

    // Если статус изменился на completed, автоматически устанавливаем дату завершения, если не указана
    if (status === 'completed' && !completionDate) {
      await query(
        `UPDATE employee_training SET completion_date = CURRENT_DATE WHERE id = $1`,
        [trainingId]
      );
    }

    // Сохраняем в историю, если статус изменился
    if (status && status !== existing[0].status) {
      await query(
        `INSERT INTO employee_training_history (
          employee_training_id, status, completion_date, score, passed, certificate_number, notes
        ) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
        [
          trainingId, status, completionDate || null, score || null,
          passed || null, certificateNumber || null, notes || null
        ]
      );
    }

    // Получаем обновленную запись с JOIN для нормализации
    const result = await query(
      `SELECT 
        et.id,
        et.employee_id AS "employeeId",
        et.program_id AS "programId",
        et.status,
        et.start_date AS "startDate",
        et.completion_date AS "completionDate",
        et.expiry_date AS "expiryDate",
        et.score,
        et.passed,
        et.certificate_number AS "certificateNumber",
        et.certificate_url AS "certificateUrl",
        et.notes,
        et.instructor_name AS "instructorName",
        et.location,
        et.created_at AS "createdAt",
        et.updated_at AS "updatedAt",
        e.name AS employee_name,
        e.position AS employee_position,
        tp.title AS program_title,
        tp.type AS program_type,
        tp.category AS program_category,
        tp.duration_hours AS program_duration_hours,
        tp.validity_months AS program_validity_months
      FROM employee_training et
      INNER JOIN employees e ON et.employee_id = e.id
      INNER JOIN training_programs tp ON et.program_id = tp.id
      WHERE et.id = $1`,
      [trainingId]
    );

    if (!result || result.length === 0) {
      return res.status(404).json({ error: 'Обучение не найдено' });
    }

    // Нормализуем поля
    const row = result[0];
    const normalized = {
      id: row.id,
      employeeId: row.employeeId,
      programId: row.programId,
      status: row.status,
      startDate: row.startDate,
      completionDate: row.completionDate,
      expiryDate: row.expiryDate,
      score: row.score,
      passed: row.passed,
      certificateNumber: row.certificateNumber,
      certificateUrl: row.certificateUrl,
      notes: row.notes,
      instructorName: row.instructorName,
      location: row.location,
      createdAt: row.createdAt,
      updatedAt: row.updatedAt,
      employeeName: row.employee_name,
      employeePosition: row.employee_position,
      programTitle: row.program_title,
      programType: row.program_type,
      programCategory: row.program_category,
      programDurationHours: row.program_duration_hours,
      programValidityMonths: row.program_validity_months
    };

    res.json(normalized);
  } catch (err) {
    console.error('Error updating training:', err);
    res.status(500).json({ error: 'Failed to update training' });
  }
});

// GET /api/training/all - получить все назначенные обучения
app.get(`${API_PREFIX}/training/all`, async (req, res) => {
  try {
    // Проверяем существование таблиц
    const tableCheck = await query(
      `SELECT table_name 
       FROM information_schema.tables 
       WHERE table_schema = 'public' 
       AND table_name = 'employee_training'`
    );

    if (!tableCheck || tableCheck.length === 0) {
      return res.json([]); // Таблица еще не создана - возвращаем пустой массив
    }

    const result = await query(
      `SELECT 
        et.id,
        et.employee_id AS "employeeId",
        et.program_id AS "programId",
        et.status,
        et.start_date AS "startDate",
        et.completion_date AS "completionDate",
        et.expiry_date AS "expiryDate",
        et.score,
        et.passed,
        et.certificate_number AS "certificateNumber",
        et.certificate_url AS "certificateUrl",
        et.notes,
        et.instructor_name AS "instructorName",
        et.location,
        et.created_at AS "createdAt",
        et.updated_at AS "updatedAt",
        e.name AS employee_name,
        e.position AS employee_position,
        tp.title AS program_title,
        tp.type AS program_type,
        tp.category AS program_category,
        tp.duration_hours AS program_duration_hours,
        tp.validity_months AS program_validity_months
      FROM employee_training et
      INNER JOIN employees e ON et.employee_id = e.id
      INNER JOIN training_programs tp ON et.program_id = tp.id
      ORDER BY et.created_at DESC`
    );

    // Нормализуем поля в camelCase
    const normalized = (result || []).map(row => ({
      id: row.id,
      employeeId: row.employeeId,
      programId: row.programId,
      status: row.status,
      startDate: row.startDate,
      completionDate: row.completionDate,
      expiryDate: row.expiryDate,
      score: row.score,
      passed: row.passed,
      certificateNumber: row.certificateNumber,
      certificateUrl: row.certificateUrl,
      notes: row.notes,
      instructorName: row.instructorName,
      location: row.location,
      createdAt: row.createdAt,
      updatedAt: row.updatedAt,
      employeeName: row.employee_name,
      employeePosition: row.employee_position,
      programTitle: row.program_title,
      programType: row.program_type,
      programCategory: row.program_category,
      programDurationHours: row.program_duration_hours,
      programValidityMonths: row.program_validity_months
    }));

    res.json(normalized);
  } catch (err) {
    console.error('Error fetching all trainings:', err);
    // Если таблица не существует, возвращаем пустой массив
    if (err.code === '42P01') {
      return res.json([]);
    }
    res.status(500).json({ error: 'Failed to fetch all trainings' });
  }
});

// GET /api/training/overdue - получить просроченные обучения
app.get(`${API_PREFIX}/training/overdue`, async (req, res) => {
  try {
    // Проверяем существование таблиц
    const tableCheck = await query(
      `SELECT table_name 
       FROM information_schema.tables 
       WHERE table_schema = 'public' 
       AND table_name = 'employee_training'`
    );

    // query() возвращает массив напрямую (res.rows), поэтому проверяем tableCheck.length
    if (!tableCheck || tableCheck.length === 0) {
      return res.json([]); // Таблица еще не создана - возвращаем пустой массив
    }

    const result = await query(
      `SELECT 
        et.id,
        et.employee_id AS "employeeId",
        et.program_id AS "programId",
        et.status,
        et.start_date AS "startDate",
        et.completion_date AS "completionDate",
        et.expiry_date AS "expiryDate",
        et.score,
        et.passed,
        et.certificate_number AS "certificateNumber",
        et.certificate_url AS "certificateUrl",
        et.notes,
        et.instructor_name AS "instructorName",
        et.location,
        et.created_at AS "createdAt",
        et.updated_at AS "updatedAt",
        e.name AS employee_name,
        e.position AS employee_position,
        tp.title AS program_title,
        tp.type AS program_type,
        tp.category AS program_category,
        tp.duration_hours AS program_duration_hours,
        tp.validity_months AS program_validity_months
      FROM employee_training et
      INNER JOIN employees e ON et.employee_id = e.id
      INNER JOIN training_programs tp ON et.program_id = tp.id
      WHERE et.expiry_date < CURRENT_DATE
        AND et.status IN ('completed', 'in_progress')
      ORDER BY et.expiry_date ASC`
    );

    // Нормализуем поля в camelCase
    const normalized = (result || []).map(row => ({
      id: row.id,
      employeeId: row.employeeId,
      programId: row.programId,
      status: row.status,
      startDate: row.startDate,
      completionDate: row.completionDate,
      expiryDate: row.expiryDate,
      score: row.score,
      passed: row.passed,
      certificateNumber: row.certificateNumber,
      certificateUrl: row.certificateUrl,
      notes: row.notes,
      instructorName: row.instructorName,
      location: row.location,
      createdAt: row.createdAt,
      updatedAt: row.updatedAt,
      employeeName: row.employee_name,
      employeePosition: row.employee_position,
      programTitle: row.program_title,
      programType: row.program_type,
      programCategory: row.program_category,
      programDurationHours: row.program_duration_hours,
      programValidityMonths: row.program_validity_months
    }));
    
    res.json(normalized);
  } catch (err) {
    console.error('Error fetching overdue training:', err);
    // Если таблица не существует, возвращаем пустой массив вместо ошибки
    if (err.code === '42P01') {
      return res.json([]);
    }
    res.status(500).json({ error: 'Failed to fetch overdue training' });
  }
});

// POST /api/training/upload-report - загрузка отчета об обучении из файла (XLS/CSV)
app.post(`${API_PREFIX}/training/upload-report`, upload.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'Файл не загружен' });
    }

    const fileType = path.extname(req.file.originalname).toLowerCase() === '.csv' ? 'CSV' : 'XLSX';
    
    // Парсим файл
    let rows = [];
    if (fileType === 'CSV') {
      rows = await fileProcessor.parseCSV(req.file.path);
    } else {
      rows = await fileProcessor.parseXLSX(req.file.path);
    }

    if (rows.length === 0) {
      return res.status(400).json({ error: 'Файл пуст или не может быть прочитан' });
    }

    const processed = [];
    const errors = [];

    // Обрабатываем каждую строку
    for (let i = 0; i < rows.length; i++) {
      const row = rows[i];
      try {
        // Ожидаемые колонки в файле:
        // ФИО, Программа, Тип, Категория, Дата начала, Дата завершения, Статус, Оценка, Срок действия (месяцев)
        const employeeName = row['ФИО'] || row['Сотрудник'] || row['Имя'] || row['employeeName'];
        const programTitle = row['Программа'] || row['Название'] || row['programTitle'];
        const programType = row['Тип'] || row['type'] || 'instruction';
        const programCategory = row['Категория'] || row['category'] || 'safety';
        const startDate = row['Дата начала'] || row['startDate'];
        const completionDate = row['Дата завершения'] || row['completionDate'];
        const status = row['Статус'] || row['status'] || 'completed';
        const score = row['Оценка'] || row['score'];
        const validityMonths = row['Срок действия'] || row['validityMonths'];

        if (!employeeName || !programTitle) {
          errors.push({
            row: i + 2,
            message: 'Не указаны ФИО сотрудника или название программы',
            data: row
          });
          continue;
        }

        // Находим сотрудника
        const employeeResult = await query(
          'SELECT id FROM employees WHERE name ILIKE $1 LIMIT 1',
          [`%${employeeName}%`]
        );

        if (employeeResult.length === 0) {
          errors.push({
            row: i + 2,
            message: `Сотрудник "${employeeName}" не найден`,
            data: row
          });
          continue;
        }

        const employeeId = employeeResult[0].id;

        // Находим или создаем программу обучения
        let programId;
        const programResult = await query(
          'SELECT id FROM training_programs WHERE title = $1 LIMIT 1',
          [programTitle]
        );

        if (programResult.length > 0) {
          programId = programResult[0].id;
        } else {
          // Создаем новую программу
          programId = `training-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
          await query(
            `INSERT INTO training_programs (id, title, type, category, validity_months)
             VALUES ($1, $2, $3, $4, $5)`,
            [programId, programTitle, programType, programCategory, validityMonths ? parseInt(validityMonths) : null]
          );
        }

        // Рассчитываем дату окончания срока действия
        let expiryDate = null;
        if (validityMonths && completionDate) {
          const completion = new Date(completionDate);
          completion.setMonth(completion.getMonth() + parseInt(validityMonths));
          expiryDate = completion.toISOString().split('T')[0];
        }

        // Создаем или обновляем запись об обучении
        const existing = await query(
          'SELECT id FROM employee_training WHERE employee_id = $1 AND program_id = $2',
          [employeeId, programId]
        );

        if (existing.length > 0) {
          // Обновляем существующую запись
          await query(
            `UPDATE employee_training SET
              status = $1,
              start_date = $2,
              completion_date = $3,
              expiry_date = $4,
              score = $5,
              passed = $6,
              updated_at = NOW()
            WHERE id = $7`,
            [
              status,
              startDate || null,
              completionDate || null,
              expiryDate,
              score ? parseFloat(score) : null,
              status === 'completed' ? true : (status === 'failed' ? false : null),
              existing[0].id
            ]
          );
        } else {
          // Создаем новую запись
          await query(
            `INSERT INTO employee_training (
              employee_id, program_id, status, start_date, completion_date, expiry_date, score, passed
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
            [
              employeeId,
              programId,
              status,
              startDate || null,
              completionDate || null,
              expiryDate,
              score ? parseFloat(score) : null,
              status === 'completed' ? true : (status === 'failed' ? false : null)
            ]
          );
        }

        processed.push({ row: i + 2, employee: employeeName, program: programTitle });

      } catch (error) {
        errors.push({
          row: i + 2,
          message: error.message,
          data: row
        });
      }
    }

    // Удаляем файл после обработки
    fs.unlinkSync(req.file.path);

    res.json({
      success: true,
      processed: processed.length,
      errors: errors.length,
      details: {
        processed,
        errors: errors.slice(0, 50) // Ограничиваем количество ошибок
      }
    });

  } catch (error) {
    console.error('Error processing training report:', error);
    res.status(500).json({ 
      error: 'Ошибка обработки отчета об обучении',
      details: error.message 
    });
  }
});

// ========= PR и NPS: ENDPOINTS =========

// ========= ОТЗЫВЫ =========

// GET /api/pr/reviews - список отзывов
app.get(`${API_PREFIX}/pr/reviews`, async (req, res) => {
  try {
    const { building_id, source, status } = req.query;
    
    // Проверяем существование таблицы перед запросом
    try {
      const tableCheck = await query('SELECT 1 FROM reviews LIMIT 1');
    } catch (tableErr) {
      if (tableErr.message?.includes('relation "reviews" does not exist') || 
          tableErr.message?.includes('does not exist')) {
        console.warn('[server] Таблица reviews не найдена. Возвращаем пустой массив.');
        return res.json([]);
      }
    }
    
    let queryText = `
      SELECT r.*, 
             CASE 
               WHEN b.data IS NOT NULL AND b.data->'passport' IS NOT NULL 
               THEN b.data->'passport'->>'address'
               ELSE NULL
             END as address
      FROM reviews r
      LEFT JOIN buildings b ON r.building_id = b.id
      WHERE 1=1
    `;
    const params = [];

    if (building_id) {
      queryText += ` AND r.building_id = $${params.length + 1}`;
      params.push(building_id);
    }
    if (source) {
      queryText += ` AND r.source = $${params.length + 1}`;
      params.push(source);
    }
    if (status) {
      queryText += ` AND r.status = $${params.length + 1}`;
      params.push(status);
    }

    queryText += ' ORDER BY r.date DESC, r.created_at DESC';

    console.log(`[server] Запрос отзывов: building_id=${building_id || 'all'}, source=${source || 'all'}, status=${status || 'all'}`);
    
    const rows = await query(queryText, params);
    console.log(`[server] Найдено отзывов: ${rows.length}`);
    
    // Преобразуем snake_case в camelCase для фронтенда
    const formattedRows = rows.map(row => {
      try {
        return {
      id: row.id,
      buildingId: row.building_id || null,
      source: row.source || 'other',
      sourceUrl: row.source_url || null,
      authorName: row.author_name || null,
      text: row.text || '',
      rating: row.rating || 5,
      date: row.date || new Date().toISOString().split('T')[0],
      status: row.status || 'new',
      processedAt: row.processed_at || null,
      processedBy: row.processed_by || null,
      createdAt: row.created_at || new Date().toISOString(),
      updatedAt: row.updated_at || new Date().toISOString(),
      address: row.address || null
    };
      } catch (mapErr) {
        console.error('[server] Ошибка преобразования строки отзыва:', mapErr, row);
        return null;
      }
    }).filter(row => row !== null);
    
    console.log(`[server] Отформатировано отзывов: ${formattedRows.length}`);
    res.json(formattedRows);
  } catch (err) {
    console.error('[server] Error fetching reviews:', err);
    console.error('[server] Stack trace:', err.stack);
    
    // Если таблица не существует, возвращаем пустой массив вместо ошибки
    if (err.message?.includes('relation "reviews" does not exist') || 
        err.message?.includes('does not exist')) {
      console.warn('[server] Таблица reviews не найдена. Возвращаем пустой массив.');
      return res.json([]);
    }
    
    res.status(500).json({ 
      error: 'Failed to fetch reviews', 
      details: err.message,
      stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
    });
  }
});

// POST /api/pr/reviews - создание отзыва
app.post(`${API_PREFIX}/pr/reviews`, async (req, res) => {
  try {
    const { building_id, source, source_url, author_name, text, rating, date } = req.body;

    if (!building_id || !text || !rating || !date) {
      return res.status(400).json({ error: 'Missing required fields' });
    }

    const result = await query(
      `INSERT INTO reviews (building_id, source, source_url, author_name, text, rating, date, status)
       VALUES ($1, $2, $3, $4, $5, $6, $7, 'new')
       RETURNING *`,
      [building_id, source || 'internal', source_url, author_name, text, rating, date]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating review:', err);
    res.status(500).json({ error: 'Failed to create review' });
  }
});

// PUT /api/pr/reviews/:id - обновление отзыва
app.put(`${API_PREFIX}/pr/reviews/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { text, rating, status, processed_by } = req.body;

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (text !== undefined) {
      updates.push(`text = $${paramIndex++}`);
      params.push(text);
    }
    if (rating !== undefined) {
      updates.push(`rating = $${paramIndex++}`);
      params.push(rating);
    }
    if (status !== undefined) {
      updates.push(`status = $${paramIndex++}`);
      params.push(status);
      if (status === 'processed' || status === 'archived') {
        updates.push(`processed_at = NOW()`);
        if (processed_by) {
          updates.push(`processed_by = $${paramIndex++}`);
          params.push(processed_by);
        }
      }
    }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    updates.push(`updated_at = NOW()`);
    params.push(id);

    const result = await query(
      `UPDATE reviews SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      params
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Review not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating review:', err);
    res.status(500).json({ error: 'Failed to update review' });
  }
});

// PUT /api/pr/reviews/:id/status - изменение статуса
app.put(`${API_PREFIX}/pr/reviews/:id/status`, async (req, res) => {
  try {
    const { id } = req.params;
    const { status, processed_by } = req.body;

    if (!status) {
      return res.status(400).json({ error: 'Status is required' });
    }

    const updates = ['status = $1', 'updated_at = NOW()'];
    const params = [status];

    if (status === 'processed' || status === 'archived') {
      updates.push('processed_at = NOW()');
      if (processed_by) {
        updates.push('processed_by = $2');
        params.push(processed_by);
      }
    }

    params.push(id);

    const result = await query(
      `UPDATE reviews SET ${updates.join(', ')} WHERE id = $${params.length} RETURNING *`,
      params
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Review not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating review status:', err);
    res.status(500).json({ error: 'Failed to update review status' });
  }
});

// GET /api/pr/reviews/stats - статистика по отзывам
app.get(`${API_PREFIX}/pr/reviews/stats`, async (req, res) => {
  try {
    const { building_id } = req.query;
    let queryText = `
      SELECT 
        COUNT(*) as total,
        COUNT(*) FILTER (WHERE status = 'new') as new_count,
        COUNT(*) FILTER (WHERE status = 'processed') as processed_count,
        COUNT(*) FILTER (WHERE status = 'archived') as archived_count,
        AVG(rating) as avg_rating,
        COUNT(*) FILTER (WHERE rating >= 7) as positive_count,
        COUNT(*) FILTER (WHERE rating <= 3) as negative_count,
        COUNT(*) FILTER (WHERE source = 'yandex_maps') as yandex_count,
        COUNT(*) FILTER (WHERE source = '2gis') as gis2_count
      FROM reviews
      WHERE 1=1
    `;
    const params = [];

    if (building_id) {
      queryText += ` AND building_id = $1`;
      params.push(building_id);
    }

    const result = await query(queryText, params);
    res.json(result[0]);
  } catch (err) {
    console.error('Error fetching review stats:', err);
    res.status(500).json({ error: 'Failed to fetch review stats' });
  }
});

// ========= ИНЦИДЕНТЫ =========

// GET /api/pr/incidents - список инцидентов
app.get(`${API_PREFIX}/pr/incidents`, async (req, res) => {
  try {
    const { building_id, status, type, assigned_to, review_id } = req.query;
    let queryText = `
      SELECT i.*, 
             b.data->'passport'->>'address' as address,
             r.text as review_text,
             r.rating as review_rating
      FROM incidents i
      LEFT JOIN buildings b ON i.building_id = b.id
      LEFT JOIN reviews r ON i.review_id = r.id
      WHERE 1=1
    `;
    const params = [];

    if (building_id) {
      queryText += ` AND i.building_id = $${params.length + 1}`;
      params.push(building_id);
    }
    if (status) {
      queryText += ` AND i.status = $${params.length + 1}`;
      params.push(status);
    }
    if (type) {
      queryText += ` AND i.type = $${params.length + 1}`;
      params.push(type);
    }
    if (assigned_to) {
      queryText += ` AND i.assigned_to = $${params.length + 1}`;
      params.push(assigned_to);
    }
    if (review_id) {
      queryText += ` AND i.review_id = $${params.length + 1}`;
      params.push(review_id);
    }

    queryText += ' ORDER BY i.created_at DESC';

    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching incidents:', err);
    res.status(500).json({ error: 'Failed to fetch incidents' });
  }
});

// POST /api/pr/incidents - создание инцидента
app.post(`${API_PREFIX}/pr/incidents`, async (req, res) => {
  try {
    const { review_id, building_id, type, title, description, priority, assigned_to, created_by } = req.body;

    if (!building_id || !type || !title || !description || !created_by) {
      return res.status(400).json({ error: 'Missing required fields' });
    }

    const result = await query(
      `INSERT INTO incidents (review_id, building_id, type, title, description, priority, assigned_to, created_by, status)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'new')
       RETURNING *`,
      [review_id || null, building_id, type, title, description, priority || 'medium', assigned_to || null, created_by]
    );

    try {
      const row = result[0];
      const opts = { type: 'incident_new', title: 'Новый инцидент', body: (title || '').slice(0, 80) + ((title || '').length > 80 ? '…' : ''), entityType: 'incident', entityId: String(row.id) };
      if (assigned_to) {
        const userIds = await notificationService.resolveEmployeeNamesToUserIds(pool, [assigned_to]);
        if (userIds.length > 0) {
          await notificationService.createNotificationForUserIds(pool, userIds, opts);
        } else {
          await notificationService.createNotificationForResponsibleZone(pool, 'pr', 'feedback', opts);
        }
      } else {
        await notificationService.createNotificationForResponsibleZone(pool, 'pr', 'feedback', opts);
      }
    } catch (notifErr) {
      console.warn('[notifications] incident POST:', notifErr.message);
    }
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating incident:', err);
    res.status(500).json({ error: 'Failed to create incident' });
  }
});

// PUT /api/pr/incidents/:id - обновление инцидента
app.put(`${API_PREFIX}/pr/incidents/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { title, description, status, priority, assigned_to, resolution_notes } = req.body;

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (title !== undefined) {
      updates.push(`title = $${paramIndex++}`);
      params.push(title);
    }
    if (description !== undefined) {
      updates.push(`description = $${paramIndex++}`);
      params.push(description);
    }
    if (status !== undefined) {
      updates.push(`status = $${paramIndex++}`);
      params.push(status);
      if (status === 'resolved' || status === 'closed') {
        updates.push(`resolved_at = NOW()`);
        if (resolution_notes) {
          updates.push(`resolution_notes = $${paramIndex++}`);
          params.push(resolution_notes);
        }
      }
    }
    if (priority !== undefined) {
      updates.push(`priority = $${paramIndex++}`);
      params.push(priority);
    }
    if (assigned_to !== undefined) {
      updates.push(`assigned_to = $${paramIndex++}`);
      params.push(assigned_to);
    }
    if (resolution_notes !== undefined && !updates.includes('resolution_notes')) {
      updates.push(`resolution_notes = $${paramIndex++}`);
      params.push(resolution_notes);
    }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    updates.push(`updated_at = NOW()`);
    params.push(id);

    const result = await query(
      `UPDATE incidents SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      params
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Incident not found' });
    }

    const row = result[0];
    try {
      const opts = { type: 'incident_assigned', title: 'Инцидент: обновление', body: (row.title || '').slice(0, 80) + ((row.title || '').length > 80 ? '…' : ''), entityType: 'incident', entityId: String(id) };
      if (assigned_to !== undefined && row.assigned_to) {
        const userIds = await notificationService.resolveEmployeeNamesToUserIds(pool, [row.assigned_to]);
        if (userIds.length > 0) {
          await notificationService.createNotificationForUserIds(pool, userIds, opts);
        } else {
          await notificationService.createNotificationForResponsibleZone(pool, 'pr', 'feedback', opts);
        }
      } else {
        await notificationService.createNotificationForResponsibleZone(pool, 'pr', 'feedback', opts);
      }
    } catch (notifErr) {
      console.warn('[notifications] incident PUT:', notifErr.message);
    }
    res.json(row);
  } catch (err) {
    console.error('Error updating incident:', err);
    res.status(500).json({ error: 'Failed to update incident' });
  }
});

// PUT /api/pr/incidents/:id/resolve - разрешение инцидента
app.put(`${API_PREFIX}/pr/incidents/:id/resolve`, async (req, res) => {
  try {
    const { id } = req.params;
    const { resolution_notes } = req.body;

    const result = await query(
      `UPDATE incidents 
       SET status = 'resolved', 
           resolved_at = NOW(),
           resolution_notes = $1,
           updated_at = NOW()
       WHERE id = $2
       RETURNING *`,
      [resolution_notes || null, id]
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Incident not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error resolving incident:', err);
    res.status(500).json({ error: 'Failed to resolve incident' });
  }
});

// POST /api/pr/incidents/from-review/:reviewId - создание инцидента из отзыва
app.post(`${API_PREFIX}/pr/incidents/from-review/:reviewId`, async (req, res) => {
  try {
    const { reviewId } = req.params;
    const { type, title, description, priority, assigned_to, created_by } = req.body;

    // Получаем отзыв
    const reviewResult = await query('SELECT * FROM reviews WHERE id = $1', [reviewId]);
    if (reviewResult.length === 0) {
      return res.status(404).json({ error: 'Review not found' });
    }

    const review = reviewResult[0];

    // Создаем инцидент
    const result = await query(
      `INSERT INTO incidents (review_id, building_id, type, title, description, priority, assigned_to, created_by, status)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'new')
       RETURNING *`,
      [
        reviewId,
        review.building_id,
        type || 'service_quality',
        title || `Инцидент по отзыву от ${review.author_name || 'пользователя'}`,
        description || review.text,
        priority || 'medium',
        assigned_to || null,
        created_by || 'System'
      ]
    );

    // Обновляем статус отзыва
    await query(
      `UPDATE reviews SET status = 'processed', processed_at = NOW() WHERE id = $1`,
      [reviewId]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating incident from review:', err);
    res.status(500).json({ error: 'Failed to create incident from review' });
  }
});

// ========= ОТЧЕТЫ ЖИТЕЛЯМ =========

// GET /api/pr/reports - список отчетов (один отчет на дом - последний)
app.get(`${API_PREFIX}/pr/reports`, async (req, res) => {
  try {
    const { building_id, status } = req.query;
    let queryText = `
      SELECT DISTINCT ON (r.building_id) r.*, 
             (b.data->'passport')->>'address' as address
      FROM resident_reports r
      LEFT JOIN buildings b ON r.building_id = b.id
      WHERE 1=1
    `;
    const params = [];

    if (building_id) {
      queryText += ` AND r.building_id = $${params.length + 1}`;
      params.push(building_id);
    }
    
    // Фильтрация по статусу: если не указан явно, показываем только опубликованные отчеты
    // или черновики текущего месяца (созданные 1 числа текущего месяца)
    if (status) {
      queryText += ` AND r.status = $${params.length + 1}`;
      params.push(status);
    } else {
      // По умолчанию показываем только опубликованные отчеты или черновики текущего месяца
      const now = new Date();
      const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
      const currentMonthStartStr = currentMonthStart.toISOString().split('T')[0];
      
      queryText += ` AND (
        r.status = 'published' 
        OR (r.status = 'draft' AND DATE(r.created_at) >= $${params.length + 1})
      )`;
      params.push(currentMonthStartStr);
    }

    // Для DISTINCT ON нужно сначала сортировать по building_id
    queryText += ' ORDER BY r.building_id, r.period_start DESC, r.created_at DESC';

    const rows = await query(queryText, params);
    
    // Преобразуем snake_case в camelCase для фронтенда
    const formattedRows = rows.map((r) => ({
      id: r.id,
      buildingId: r.building_id,
      address: r.address,
      month: r.month,
      periodStart: r.period_start,
      periodEnd: r.period_end,
      status: r.status,
      publishedAt: r.published_at,
      content: (() => {
        try {
          if (typeof r.content === 'string') {
            return r.content ? JSON.parse(r.content) : {};
          }
          return r.content || {};
        } catch (e) {
          console.warn('Error parsing report content:', e);
          return {};
        }
      })(),
      createdAt: r.created_at,
      updatedAt: r.updated_at
    }));

    // Живая статистика по каждому отчёту (заявки, NPS, собрано) — актуальные данные из БД
    const liveStatsPromises = formattedRows.map(async (row) => {
      const buildingId = row.buildingId;
      const periodStart = row.periodStart ? new Date(row.periodStart) : null;
      const periodEnd = row.periodEnd ? new Date(row.periodEnd) : null;
      if (!buildingId || !periodStart || !periodEnd) {
        return { applicationsTotal: 0, npsScore: 0, fundsCollected: 0 };
      }
      const periodStartStr = periodStart.toISOString().slice(0, 10);
      const periodEndStr = periodEnd.toISOString().slice(0, 10);
      try {
        const [appsRes, npsRes, finRes] = await Promise.all([
          query(
            `SELECT COUNT(*) as total FROM applications
             WHERE building_id = $1 AND created_at::date >= $2::date AND created_at::date <= $3::date`,
            [buildingId, periodStartStr, periodEndStr]
          ),
          query(
            `SELECT total_responses, nps_score, promoters, detractors FROM nps_building_stats
             WHERE building_id = $1 AND period_start = $2 AND period_end = $3 LIMIT 1`,
            [buildingId, periodStartStr, periodEndStr]
          ).then(async (npsRows) => {
            if (npsRows.length > 0 && parseInt(npsRows[0].total_responses) > 0) return npsRows;
            const liveNps = await query(
              `SELECT COUNT(*) as total_responses,
                      COUNT(*) FILTER (WHERE score >= 9) as promoters,
                      COUNT(*) FILTER (WHERE score <= 6) as detractors
               FROM nps_responses
               WHERE building_id = $1 AND (created_at::date >= $2::date AND created_at::date <= $3::date)`,
              [buildingId, periodStartStr, periodEndStr]
            );
            if (liveNps.length > 0 && parseInt(liveNps[0].total_responses) > 0) {
              const t = parseInt(liveNps[0].total_responses);
              const p = parseInt(liveNps[0].promoters) || 0;
              const d = parseInt(liveNps[0].detractors) || 0;
              return [{ nps_score: Math.round((p / t * 100) - (d / t * 100)) }];
            }
            return [{ nps_score: 0 }];
          }),
          query(
            `SELECT COALESCE(SUM(total_income), 0) as total_income
             FROM building_financial_data
             WHERE building_id = $1 AND period_start::date <= $3::date AND period_end::date >= $2::date`,
            [buildingId, periodStartStr, periodEndStr]
          )
        ]);
        const applicationsTotal = appsRes.length > 0 ? parseInt(appsRes[0].total) || 0 : 0;
        const npsScore = npsRes.length > 0 ? parseInt(npsRes[0].nps_score) || 0 : 0;
        const fundsCollected = finRes.length > 0 ? parseFloat(finRes[0].total_income) || 0 : 0;
        return { applicationsTotal, npsScore, fundsCollected };
      } catch (e) {
        console.warn('[PR reports] liveStats for report', row.id, e.message);
        return { applicationsTotal: 0, npsScore: 0, fundsCollected: 0 };
      }
    });
    const liveStatsList = await Promise.all(liveStatsPromises);
    formattedRows.forEach((row, i) => {
      row.liveStats = liveStatsList[i] || { applicationsTotal: 0, npsScore: 0, fundsCollected: 0 };
    });
    
    res.json(formattedRows);
  } catch (err) {
    console.error('Error fetching reports:', err);
    res.status(500).json({ error: 'Failed to fetch reports' });
  }
});

// GET /api/pr/reports/:id - детали отчета
app.get(`${API_PREFIX}/pr/reports/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    
    const result = await query(
      `SELECT r.*, 
              b.data->'passport'->>'address' as address,
              b.data as building_data
       FROM resident_reports r
       LEFT JOIN buildings b ON r.building_id = b.id
       WHERE r.id = $1`,
      [id]
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Report not found' });
    }

    const report = result[0];
    
    // Преобразуем content из JSONB в объект, если это строка
    if (report.content && typeof report.content === 'string') {
      try {
        report.content = JSON.parse(report.content);
      } catch (e) {
        console.warn('Error parsing report content:', e);
      }
    }

    // Если в контенте все нули — пробуем перегенерировать из building_financial_data (последние ненулевые данные по дому)
    const contentHasZeros = report.content && (() => {
      const c = report.content;
      const total = c.totals?.totalExpenses ?? 0;
      const hasServices = c.services?.length > 0 && c.services.some(s => (s.accrued || s.paid) > 0);
      const hasExpenseItems = c.expenseItems?.length > 0 && c.expenseItems.some(e => (e.total || e.perMonth) > 0);
      return total === 0 && !hasServices && !hasExpenseItems;
    })();
    if (contentHasZeros && report.building_id) {
      const fallback = await query(
        `SELECT bfd.*, b.data->'passport'->>'address' as address
         FROM building_financial_data bfd
         JOIN buildings b ON bfd.building_id = b.id
         WHERE bfd.building_id = $1
           AND (bfd.total_expenses > 0 OR (bfd.expenses_by_items IS NOT NULL AND bfd.expenses_by_items != '{}'::jsonb))
         ORDER BY bfd.period_start DESC
         LIMIT 1`,
        [report.building_id]
      );
      if (fallback.length > 0) {
        const data = fallback[0];
        const reportRow = await query('SELECT * FROM financial_reports WHERE id = $1', [data.report_id || 0]);
        const reportForContent = reportRow.length > 0 ? reportRow[0] : { id: data.report_id };
        report.content = await generateResidentReportContent(data, reportForContent);
        report.period_start = data.period_start;
        report.period_end = data.period_end;
        report.month = formatMonthFromPeriod(data.period_start, data.period_end);
        // Сохраняем перегенерированный контент в БД
        await query(
          `UPDATE resident_reports SET content = $1, period_start = $2, period_end = $3, month = $4, updated_at = NOW() WHERE id = $5`,
          [JSON.stringify(report.content), data.period_start, data.period_end, report.month, id]
        );
      }
    }
    
    // Преобразуем snake_case в camelCase для фронтенда
    const formattedReport = {
      id: report.id,
      buildingId: report.building_id,
      address: report.address,
      month: report.month,
      periodStart: report.period_start,
      periodEnd: report.period_end,
      status: report.status,
      publishedAt: report.published_at,
      content: report.content,
      createdAt: report.created_at,
      updatedAt: report.updated_at
    };

    res.json(formattedReport);
  } catch (err) {
    console.error('Error fetching report:', err);
    res.status(500).json({ error: 'Failed to fetch report' });
  }
});

// POST /api/pr/reports - создание отчета
app.post(`${API_PREFIX}/pr/reports`, async (req, res) => {
  try {
    const { building_id, month, period_start, period_end, content } = req.body;

    if (!building_id || !month || !period_start || !period_end) {
      return res.status(400).json({ error: 'Missing required fields' });
    }

    const result = await query(
      `INSERT INTO resident_reports (building_id, month, period_start, period_end, content, status)
       VALUES ($1, $2, $3, $4, $5, 'draft')
       RETURNING *`,
      [building_id, month, period_start, period_end, content ? JSON.stringify(content) : null]
    );

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating report:', err);
    res.status(500).json({ error: 'Failed to create report' });
  }
});

// PUT /api/pr/reports/:id - обновление отчета
app.put(`${API_PREFIX}/pr/reports/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { month, period_start, period_end, content, status } = req.body;

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (month !== undefined) {
      updates.push(`month = $${paramIndex++}`);
      params.push(month);
    }
    if (period_start !== undefined) {
      updates.push(`period_start = $${paramIndex++}`);
      params.push(period_start);
    }
    if (period_end !== undefined) {
      updates.push(`period_end = $${paramIndex++}`);
      params.push(period_end);
    }
    if (content !== undefined) {
      updates.push(`content = $${paramIndex++}`);
      params.push(JSON.stringify(content));
    }
    if (status !== undefined) {
      updates.push(`status = $${paramIndex++}`);
      params.push(status);
      if (status === 'published') {
        updates.push(`published_at = NOW()`);
      }
    }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    updates.push(`updated_at = NOW()`);
    params.push(id);

    const result = await query(
      `UPDATE resident_reports SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      params
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Report not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error updating report:', err);
    res.status(500).json({ error: 'Failed to update report' });
  }
});

// POST /api/pr/reports/:id/generate - генерация контента отчета (AI)
app.post(`${API_PREFIX}/pr/reports/:id/generate`, async (req, res) => {
  try {
    const { id } = req.params;
    
    // Получаем отчет
    const reportResult = await query('SELECT * FROM resident_reports WHERE id = $1', [id]);
    if (reportResult.length === 0) {
      return res.status(404).json({ error: 'Report not found' });
    }

    const report = reportResult[0];
    
    // Собираем данные для генерации
    // Заявки за период
    const applications = await query(
      `SELECT * FROM applications 
       WHERE address = (SELECT data->>'passport'->>'address' FROM buildings WHERE id = $1)
       AND created_at >= $2 AND created_at <= $3`,
      [report.building_id, report.period_start, report.period_end]
    );

    // Финансовые данные
    const financialData = await query(
      `SELECT * FROM building_financial_data 
       WHERE building_id = $1 
       AND period_start <= $2 AND period_end >= $3
       ORDER BY period_start DESC LIMIT 1`,
      [report.building_id, report.period_end, report.period_start]
    );

    // Фото отчеты
    const workPhotos = await query(
      `SELECT * FROM work_photos 
       WHERE building_id = $1 
       AND work_date >= $2 AND work_date <= $3`,
      [report.building_id, report.period_start, report.period_end]
    );

    // Генерируем контент (заглушка - в реальности здесь будет AI)
    const generatedText = `Отчет за период ${report.month}. 
Выполнено заявок: ${applications.length}. 
Фото отчетов: ${workPhotos.length}.`;

    // Обновляем контент отчета
    const content = report.content ? JSON.parse(report.content) : {};
    content.generatedText = generatedText;
    content.applications = {
      total: applications.length,
      completed: applications.filter(a => a.status === 'done').length,
      inProgress: applications.filter(a => a.status === 'in_progress').length,
      new: applications.filter(a => a.status === 'new').length
    };
    content.workPhotos = workPhotos;

    if (financialData.length > 0) {
      const fin = financialData[0];
      content.finances = {
        income: parseFloat(fin.total_income) || 0,
        expenses: parseFloat(fin.total_expenses) || 0,
        balance: parseFloat(fin.balance) || 0
      };
    }

    await query(
      `UPDATE resident_reports SET content = $1, updated_at = NOW() WHERE id = $2`,
      [JSON.stringify(content), id]
    );

    res.json({ success: true, content });
  } catch (err) {
    console.error('Error generating report content:', err);
    res.status(500).json({ error: 'Failed to generate report content' });
  }
});

// POST /api/pr/reports/bulk-create - массовое создание отчетов для всех домов
app.post(`${API_PREFIX}/pr/reports/bulk-create`, async (req, res) => {
  try {
    const { month, period_start, period_end, building_ids } = req.body;

    if (!period_start || !period_end) {
      return res.status(400).json({ error: 'period_start and period_end are required' });
    }

    // Получаем список домов
    let targetBuildings;
    if (building_ids && Array.isArray(building_ids) && building_ids.length > 0) {
      const placeholders = building_ids.map((_, i) => `$${i + 1}`).join(', ');
      const result = await query(
        `SELECT id FROM buildings WHERE id IN (${placeholders})`,
        building_ids
      );
      targetBuildings = result.map(row => row.id);
    } else {
      const result = await query('SELECT id FROM buildings');
      targetBuildings = result.map(row => row.id);
    }

    const createdReports = [];

    // Создаем отчеты для каждого дома
    for (const buildingId of targetBuildings) {
      try {
        // Проверяем, существует ли уже отчет за этот период
        const existing = await query(
          `SELECT id FROM resident_reports 
           WHERE building_id = $1 AND period_start = $2 AND period_end = $3`,
          [buildingId, period_start, period_end]
        );

        if (existing.length > 0) {
          // Обновляем существующий отчет
          const reportId = existing[0].id;
          
          // Генерируем данные для отчета
          const reportData = await query(
            `SELECT * FROM resident_reports WHERE id = $1`,
            [reportId]
          );

          if (reportData.length > 0) {
            // Обновляем месяц, если изменился
            await query(
              `UPDATE resident_reports 
               SET month = $1, updated_at = NOW()
               WHERE id = $2`,
              [month || formatMonthFromPeriod(period_start, period_end), reportId]
            );
          }

          createdReports.push({ buildingId, reportId, updated: true });
        } else {
          // Создаем новый отчет
          const reportMonth = month || formatMonthFromPeriod(period_start, period_end);
          
          const newReport = await query(
            `INSERT INTO resident_reports 
             (building_id, month, period_start, period_end, status, content)
             VALUES ($1, $2, $3, $4, 'draft', '{}'::jsonb)
             RETURNING id`,
            [buildingId, reportMonth, period_start, period_end]
          );

          const reportId = newReport[0].id;

          // Генерируем контент отчета
          try {
            await query(
              `UPDATE resident_reports 
               SET content = (
                 SELECT jsonb_build_object(
                   'building', jsonb_build_object(
                     'id', b.id,
                     'address', COALESCE(b.data->'passport'->>'address', b.data->'passport'->'general'->>'address', 'Адрес не указан'),
                     'imageUrl', COALESCE(b.data->>'imageUrl', '')
                   ),
                   'period', jsonb_build_object(
                     'start', $2::text,
                     'end', $3::text
                   ),
                   'stats', jsonb_build_object(
                     'appsQuality', 0,
                     'appsTotal', 0,
                     'appsCompleted', 0,
                     'tasksTotal', 0,
                     'tasksCompleted', 0,
                     'fundsCollected', 0,
                     'fundsSpent', 0,
                     'fundsBalance', 0,
                     'debtCasesWon', 0,
                     'debtCollected', 0
                   ),
                   'expenses', jsonb_build_object(
                     'total', 0,
                     'byCategory', jsonb_build_object(
                       'utilities', 0,
                       'maintenance', 0,
                       'management', 0,
                       'other', 0
                     )
                   ),
                   'events', '[]'::jsonb,
                   'nps', jsonb_build_object(
                     'score', 0,
                     'avgScore', 0,
                     'totalResponses', 0,
                     'promoters', 0,
                     'passives', 0,
                     'detractors', 0
                   ),
                   'workPhotos', '[]'::jsonb,
                   'planItems', '[]'::jsonb
                 )
                 FROM buildings b
                 WHERE b.id = $1
               ),
               updated_at = NOW()
               WHERE id = $4`,
              [buildingId, period_start, period_end, reportId]
            );
          } catch (genErr) {
            console.error(`Error generating content for report ${reportId}:`, genErr);
            // Продолжаем, даже если генерация контента не удалась
          }

          createdReports.push({ buildingId, reportId, updated: false });
        }
      } catch (err) {
        console.error(`Error creating report for building ${buildingId}:`, err);
        // Продолжаем создание отчетов для других домов
      }
    }

    res.json({
      success: true,
      reportsCreated: createdReports.length,
      reports: createdReports
    });
  } catch (err) {
    console.error('Error bulk creating reports:', err);
    res.status(500).json({ error: 'Failed to create reports', details: err.message });
  }
});

// Вспомогательная функция для форматирования месяца
function formatMonthFromPeriod(start, end) {
  try {
    const startDate = new Date(start);
    const months = [
      'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
      'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
    ];
    return `${months[startDate.getMonth()]} ${startDate.getFullYear()}`;
  } catch {
    return 'Неизвестный период';
  }
}

// POST /api/pr/reports/:id/publish - публикация отчета (обновляет данные и публикует)
app.post(`${API_PREFIX}/pr/reports/:id/publish`, async (req, res) => {
  try {
    const { id } = req.params;

    // Получаем отчет
    const reportResult = await query('SELECT * FROM resident_reports WHERE id = $1', [id]);
    if (reportResult.length === 0) {
      return res.status(404).json({ error: 'Report not found' });
    }

    const report = reportResult[0];
    
    // Обновляем данные отчета перед публикацией
    // Используем логику из GET /api/pr/reports/:id/data для получения актуальных данных
    const periodStart = new Date(report.period_start);
    const periodEnd = new Date(report.period_end);
    
    // Собираем актуальные данные (упрощенная версия логики из /data endpoint)
    const buildingResult = await query('SELECT * FROM buildings WHERE id = $1', [report.building_id]);
    const building = buildingResult.length > 0 ? buildingResult[0] : null;
    const buildingData = building?.data || {};
    const buildingAddress = buildingData?.passport?.address || buildingData?.passport?.general?.address || 'Адрес не указан';
    const buildingImage = building?.data?.imageUrl || building?.imageUrl || '';

    // Качество отработки заявок
    const appsResult = await query(
      `SELECT 
        COUNT(*) as total,
        COUNT(*) FILTER (WHERE status = 'done' AND completed_at IS NOT NULL) as completed,
        COUNT(*) FILTER (WHERE status = 'done' AND completed_at <= deadline_at) as completed_on_time
       FROM applications
       WHERE building_id = $1 
         AND created_at >= $2 
         AND created_at <= $3`,
      [report.building_id, periodStart, periodEnd]
    );
    const appsStats = appsResult.length > 0 ? appsResult[0] : { total: 0, completed: 0, completed_on_time: 0 };
    const appsQuality = appsStats.total > 0 ? Math.round((appsStats.completed_on_time / appsStats.total) * 100) : 0;

    // Задачи
    const tasksResult = await query(
      `SELECT COUNT(*) as total, 
              COUNT(*) FILTER (WHERE status = 'done') as completed
       FROM applications
       WHERE building_id = $1 
         AND created_at >= $2 
         AND created_at <= $3
         AND category IS NOT NULL`,
      [report.building_id, periodStart, periodEnd]
    );
    const tasksStats = tasksResult.length > 0 ? tasksResult[0] : { total: 0, completed: 0 };

    // Финансы
    const financesResult = await query(
      `SELECT 
        COALESCE(SUM(total_income), 0) as total_income,
        COALESCE(SUM(total_expenses), 0) as total_expenses,
        COALESCE(SUM(balance), 0) as balance
       FROM building_financial_data
       WHERE building_id = $1 
         AND period_start <= $3 
         AND period_end >= $2`,
      [report.building_id, periodStart, periodEnd]
    );
    const finances = financesResult.length > 0 ? financesResult[0] : { total_income: 0, total_expenses: 0, balance: 0 };

    // Долги
    const debtResult = await query(
      `SELECT 
        COUNT(*) as cases_won,
        COALESCE(SUM(CAST(metadata->>'debt_collected' AS NUMERIC)), 0) as debt_collected
       FROM applications
       WHERE building_id = $1 
         AND category = 'debt_collection'
         AND status = 'done'
         AND created_at >= $2 
         AND created_at <= $3`,
      [report.building_id, periodStart, periodEnd]
    );
    const debtStats = debtResult.length > 0 ? debtResult[0] : { cases_won: 0, debt_collected: 0 };

    // Расходы по категориям
    const expensesResult = await query(
      `SELECT expenses_by_items
       FROM building_financial_data
       WHERE building_id = $1 
         AND period_start <= $3 
         AND period_end >= $2
       ORDER BY period_start DESC
       LIMIT 1`,
      [report.building_id, periodStart, periodEnd]
    );
    
    let expensesByCategory = { utilities: 0, maintenance: 0, management: 0, other: 0 };
    if (expensesResult.length > 0 && expensesResult[0].expenses_by_items) {
      const expenses = expensesResult[0].expenses_by_items;
      const utilitiesKeywords = ['электроэнергия', 'электричество', 'водоснабжение', 'водоотведение', 'отопление', 'газ', 'коммунальные'];
      const maintenanceKeywords = ['ремонт', 'содержание', 'лифт', 'уборка', 'мусор', 'материалы', 'благоустройство', 'техническое обслуживание', 'инженерное оборудование', 'общедомовое имущество', 'механизированная уборка', 'расходные материалы'];
      const managementKeywords = ['зарплата', 'персонал', 'административ', 'управленческ', 'страхование', 'управление', 'судебные издержки', 'услуги специализирован'];

      Object.entries(expenses).forEach(([item, amount]) => {
        const itemLower = item.toLowerCase();
        const amountNum = parseFloat(amount) || 0;
        
        if (utilitiesKeywords.some(kw => itemLower.includes(kw))) {
          expensesByCategory.utilities += amountNum;
        } else if (maintenanceKeywords.some(kw => itemLower.includes(kw))) {
          expensesByCategory.maintenance += amountNum;
        } else if (managementKeywords.some(kw => itemLower.includes(kw))) {
          expensesByCategory.management += amountNum;
        } else {
          expensesByCategory.other += amountNum;
        }
      });
    }

    // Мероприятия — только из модуля PR Мероприятия (pr_events)
    const periodStartStr = periodStart.toISOString().slice(0, 10);
    const periodEndStr = periodEnd.toISOString().slice(0, 10);
    let events = [];
    try {
      let prEventsResult = [];
      try {
        prEventsResult = await query(
          `SELECT id, title, date, type, category, status, location, attendees_count, budget, short_plan AS "shortPlan", announcement
           FROM pr_events
           WHERE date >= $2::date AND date <= $3::date
             AND (
               location_building_id = $1
               OR EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(location_building_ids, '[]'::jsonb)) AS elem WHERE elem::text = $1)
               OR (location_district_id IS NOT NULL AND location_district_id = (SELECT data->>'districtId' FROM buildings WHERE id = $1 LIMIT 1))
             )
           ORDER BY date DESC
           LIMIT 20`,
          [report.building_id, periodStartStr, periodEndStr]
        );
      } catch (qErr) {
        prEventsResult = await query(
          `SELECT id, title, date, type, category, status, location, attendees_count, budget, short_plan AS "shortPlan", announcement
           FROM pr_events
           WHERE location_building_id = $1 AND date >= $2::date AND date <= $3::date
           ORDER BY date DESC
           LIMIT 20`,
          [report.building_id, periodStartStr, periodEndStr]
        );
      }
      events = (prEventsResult || []).map(row => ({
        id: row.id,
        title: row.title || '',
        date: row.date,
        description: (row.shortPlan || row.announcement) || '',
        location: row.location || buildingAddress,
        attendeesCount: row.attendees_count != null ? parseInt(row.attendees_count, 10) : 0
      }));
    } catch (e) {
      console.warn('[Report] events (pr_events):', e.message);
    }

    // NPS
    const npsResult = await query(
      `SELECT 
        COUNT(*) as total_responses,
        AVG(score)::numeric(3,1) as avg_score,
        COUNT(*) FILTER (WHERE score >= 9) as promoters,
        COUNT(*) FILTER (WHERE score >= 7 AND score <= 8) as passives,
        COUNT(*) FILTER (WHERE score <= 6) as detractors
       FROM nps_responses
       WHERE building_id = $1 
         AND created_at >= $2 
         AND created_at <= $3`,
      [report.building_id, periodStart, periodEnd]
    );
    const npsStats = npsResult.length > 0 ? npsResult[0] : { total_responses: 0, avg_score: 0, promoters: 0, passives: 0, detractors: 0 };
    const nps = npsStats.total_responses > 0 
      ? Math.round((npsStats.promoters / npsStats.total_responses * 100) - (npsStats.detractors / npsStats.total_responses * 100))
      : 0;

    // Фото отчеты (до/после по задачам, привязка — task_id, опц. taskTitle из applications)
    const photosResult = await query(
      `SELECT w.id, w.work_name, w.work_date, w.description, w.photo_before_url, w.photo_after_url, w.task_id,
              a.description AS task_title, a.number AS task_number
       FROM work_photos w
       LEFT JOIN applications a ON (a.id::text = w.task_id OR a.number = w.task_id)
       WHERE w.building_id = $1 
         AND w.work_date >= $2 
         AND w.work_date <= $3
       ORDER BY w.work_date DESC`,
      [report.building_id, periodStart, periodEnd]
    );
    const workPhotos = photosResult.map(row => ({
      id: row.id,
      workName: row.work_name,
      workDate: row.work_date,
      description: row.description,
      photoBeforeUrl: row.photo_before_url,
      photoAfterUrl: row.photo_after_url,
      taskId: row.task_id || null,
      taskTitle: row.task_title || row.task_number || null
    }));

    // План работ на следующий месяц
    const nextMonthStart = new Date(periodEnd);
    nextMonthStart.setMonth(nextMonthStart.getMonth() + 1);
    nextMonthStart.setDate(1);
    const nextMonthEnd = new Date(nextMonthStart.getFullYear(), nextMonthStart.getMonth() + 1, 0);

    const planResult = await query(
      `SELECT id, title, description, deadline_at, category, status
       FROM applications
       WHERE building_id = $1 
         AND deadline_at >= $2 
         AND deadline_at <= $3
         AND status != 'done'
       ORDER BY deadline_at ASC
       LIMIT 10`,
      [report.building_id, nextMonthStart, nextMonthEnd]
    );
    const planItems = planResult.map(row => ({
      id: row.id,
      title: row.title || row.description,
      description: row.description,
      deadline: row.deadline_at,
      category: row.category,
      status: row.status
    }));

    // Настройки компании
    const companyResult = await query('SELECT * FROM company_settings WHERE id = 1 LIMIT 1');
    const company = companyResult.length > 0 ? companyResult[0] : null;

    // Формируем обновленный контент
    const updatedContent = {
      building: {
        id: report.building_id,
        address: buildingAddress,
        imageUrl: buildingImage
      },
      company: company ? {
        name: company.name,
        fullName: company.full_name,
        address: company.address,
        phone: company.phone,
        email: company.email,
        website: company.website,
        licenseNumber: company.license_number,
        licenseValidUntil: company.license_valid_until,
        logoUrl: company.logo_url
      } : null,
      period: {
        start: periodStart.toISOString(),
        end: periodEnd.toISOString()
      },
      stats: {
        appsQuality: appsQuality,
        appsTotal: parseInt(appsStats.total) || 0,
        appsCompleted: parseInt(appsStats.completed) || 0,
        tasksTotal: parseInt(tasksStats.total) || 0,
        tasksCompleted: parseInt(tasksStats.completed) || 0,
        fundsCollected: parseFloat(finances.total_income) || 0,
        fundsSpent: parseFloat(finances.total_expenses) || 0,
        fundsBalance: parseFloat(finances.balance) || 0,
        debtCasesWon: parseInt(debtStats.cases_won) || 0,
        debtCollected: parseFloat(debtStats.debt_collected) || 0
      },
      expenses: {
        total: parseFloat(finances.total_expenses) || 0,
        byCategory: expensesByCategory,
        details: expensesResult.length > 0 && expensesResult[0].expenses_by_items ? expensesResult[0].expenses_by_items : {}
      },
      events: events,
      nps: {
        score: nps,
        avgScore: parseFloat(npsStats.avg_score) || 0,
        totalResponses: parseInt(npsStats.total_responses) || 0,
        promoters: parseInt(npsStats.promoters) || 0,
        passives: parseInt(npsStats.passives) || 0,
        detractors: parseInt(npsStats.detractors) || 0
      },
      workPhotos: workPhotos,
      planItems: planItems
    };

    // Сохраняем NPS цифры в nps_building_stats для отображения в отчетах
    if (npsStats.total_responses > 0) {
      const periodStartDate = periodStart.toISOString().slice(0, 10);
      const periodEndDate = periodEnd.toISOString().slice(0, 10);
      const promoterPct = (npsStats.promoters / npsStats.total_responses * 100).toFixed(2);
      const detractorPct = (npsStats.detractors / npsStats.total_responses * 100).toFixed(2);
      await query(
        `INSERT INTO nps_building_stats 
         (building_id, period_start, period_end, total_responses, nps_score, avg_score, promoters, passives, detractors, promoter_percent, detractor_percent, updated_at)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())
         ON CONFLICT (building_id, period_start, period_end) 
         DO UPDATE SET total_responses = $4, nps_score = $5, avg_score = $6, promoters = $7, passives = $8, detractors = $9, promoter_percent = $10, detractor_percent = $11, updated_at = NOW()`,
        [report.building_id, periodStartDate, periodEndDate, npsStats.total_responses, nps, parseFloat(npsStats.avg_score) || 0, npsStats.promoters, npsStats.passives, npsStats.detractors, promoterPct, detractorPct]
      ).catch(err => console.warn('[Publish] Ошибка сохранения NPS в nps_building_stats:', err.message));
    }

    // Сохраняем снимок отчета в resident_report_data (дом + период: например дом 12, январь)
    const periodStartDate = periodStart.toISOString().slice(0, 10);
    const periodEndDate = periodEnd.toISOString().slice(0, 10);
    const periodMonth = periodStart.getMonth() + 1;
    const periodYear = periodStart.getFullYear();
    const expensesTotalPublish = parseFloat(finances.total_expenses) || 0;
    await query(
      `INSERT INTO resident_report_data (
        building_id, period_start, period_end, period_month, period_year,
        nps_score, nps_total_responses, nps_avg_score, nps_promoters, nps_passives, nps_detractors,
        apps_total, apps_completed, apps_quality, tasks_total, tasks_completed,
        funds_collected, funds_spent, funds_balance, debt_cases_won, debt_collected,
        expenses_total, expenses_by_category, events, work_photos, plan_items, snapshot, updated_at
      ) VALUES (
        $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16,
        $17, $18, $19, $20, $21, $22, $23::jsonb, $24::jsonb, $25::jsonb, $26::jsonb, $27::jsonb, NOW()
      ) ON CONFLICT (building_id, period_start, period_end) DO UPDATE SET
        nps_score = EXCLUDED.nps_score, nps_total_responses = EXCLUDED.nps_total_responses, nps_avg_score = EXCLUDED.nps_avg_score,
        nps_promoters = EXCLUDED.nps_promoters, nps_passives = EXCLUDED.nps_passives, nps_detractors = EXCLUDED.nps_detractors,
        apps_total = EXCLUDED.apps_total, apps_completed = EXCLUDED.apps_completed, apps_quality = EXCLUDED.apps_quality,
        tasks_total = EXCLUDED.tasks_total, tasks_completed = EXCLUDED.tasks_completed,
        funds_collected = EXCLUDED.funds_collected, funds_spent = EXCLUDED.funds_spent, funds_balance = EXCLUDED.funds_balance,
        debt_cases_won = EXCLUDED.debt_cases_won, debt_collected = EXCLUDED.debt_collected,
        expenses_total = EXCLUDED.expenses_total, expenses_by_category = EXCLUDED.expenses_by_category,
        events = EXCLUDED.events, work_photos = EXCLUDED.work_photos, plan_items = EXCLUDED.plan_items, snapshot = EXCLUDED.snapshot, updated_at = NOW()`,
      [
        report.building_id, periodStartDate, periodEndDate, periodMonth, periodYear,
        nps, parseInt(npsStats.total_responses) || 0, parseFloat(npsStats.avg_score) || 0, parseInt(npsStats.promoters) || 0, parseInt(npsStats.passives) || 0, parseInt(npsStats.detractors) || 0,
        parseInt(appsStats.total) || 0, parseInt(appsStats.completed) || 0, appsQuality, parseInt(tasksStats.total) || 0, parseInt(tasksStats.completed) || 0,
        parseFloat(finances.total_income) || 0, parseFloat(finances.total_expenses) || 0, parseFloat(finances.balance) || 0,
        parseInt(debtStats.cases_won) || 0, parseFloat(debtStats.debt_collected) || 0,
        expensesTotalPublish, JSON.stringify(expensesByCategory), JSON.stringify(events), JSON.stringify(workPhotos), JSON.stringify(planItems), JSON.stringify(updatedContent)
      ]
    ).catch(err => console.warn('[Publish] Ошибка сохранения в resident_report_data:', err.message));

    // Обновляем отчет с актуальными данными и публикуем
    const result = await query(
      `UPDATE resident_reports 
       SET status = 'published', 
           published_at = NOW(),
           content = $1,
           updated_at = NOW()
       WHERE id = $2
       RETURNING *`,
      [JSON.stringify(updatedContent), id]
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Report not found' });
    }

    res.json(result[0]);
  } catch (err) {
    console.error('Error publishing report:', err);
    res.status(500).json({ error: 'Failed to publish report', details: err.message });
  }
});

// ========= ФОТО ОТЧЕТЫ РАБОТ =========

// Настройка multer для загрузки фото работ
const workPhotosDir = path.join(__dirname, 'uploads', 'work-photos');
if (!fs.existsSync(workPhotosDir)) {
  fs.mkdirSync(workPhotosDir, { recursive: true });
}

const workPhotoStorage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, workPhotosDir);
  },
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname).toLowerCase();
    const uniqueName = `${uuidv4()}${ext}`;
    cb(null, uniqueName);
  }
});

const uploadWorkPhoto = multer({
  storage: workPhotoStorage,
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
    const ext = path.extname(file.originalname).toLowerCase();
    if (allowedTypes.includes(ext)) {
      cb(null, true);
    } else {
      cb(new Error('Неподдерживаемый тип файла. Разрешены только изображения'));
    }
  },
  limits: {
    fileSize: 10 * 1024 * 1024 // 10MB
  }
});

// Multer для фото мероприятий (pr_event_photos)
const eventPhotosDir = path.join(__dirname, 'uploads', 'event-photos');
if (!fs.existsSync(eventPhotosDir)) {
  fs.mkdirSync(eventPhotosDir, { recursive: true });
}
const eventPhotoStorage = multer.diskStorage({
  destination: (req, file, cb) => { cb(null, eventPhotosDir); },
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname).toLowerCase() || '.jpg';
    cb(null, `${uuidv4()}${ext}`);
  }
});
const uploadEventPhoto = multer({
  storage: eventPhotoStorage,
  fileFilter: (req, file, cb) => {
    const allowed = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
    if (allowed.includes(path.extname(file.originalname).toLowerCase())) cb(null, true);
    else cb(new Error('Только изображения'));
  },
  limits: { fileSize: 10 * 1024 * 1024 }
});

// Multer для изображений постов (pr_scheduled_posts)
const postImagesDir = path.join(__dirname, 'uploads', 'post-images');
if (!fs.existsSync(postImagesDir)) {
  fs.mkdirSync(postImagesDir, { recursive: true });
}
const postImageStorage = multer.diskStorage({
  destination: (req, file, cb) => { cb(null, postImagesDir); },
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname).toLowerCase() || '.jpg';
    cb(null, `${uuidv4()}${ext}`);
  }
});
const uploadPostImage = multer({
  storage: postImageStorage,
  fileFilter: (req, file, cb) => {
    const allowed = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
    if (allowed.includes(path.extname(file.originalname).toLowerCase())) cb(null, true);
    else cb(new Error('Только изображения'));
  },
  limits: { fileSize: 10 * 1024 * 1024 }
});

// GET /api/pr/work-photos - список фото отчетов
app.get(`${API_PREFIX}/pr/work-photos`, async (req, res) => {
  try {
    const { building_id, resident_report_id, task_id } = req.query;
    let queryText = `
      SELECT w.*, 
             b.data->'passport'->>'address' as address
      FROM work_photos w
      LEFT JOIN buildings b ON w.building_id = b.id
      WHERE 1=1
    `;
    const params = [];

    if (building_id) {
      queryText += ` AND w.building_id = $${params.length + 1}`;
      params.push(building_id);
    }
    if (resident_report_id) {
      queryText += ` AND w.resident_report_id = $${params.length + 1}`;
      params.push(resident_report_id);
    }
    if (task_id) {
      queryText += ` AND w.task_id = $${params.length + 1}`;
      params.push(task_id);
    }

    queryText += ' ORDER BY w.work_date DESC, w.created_at DESC';

    const rows = await query(queryText, params);
    const mapped = rows.map(r => ({
      id: r.id,
      buildingId: r.building_id,
      residentReportId: r.resident_report_id || undefined,
      taskId: r.task_id || undefined,
      workName: r.work_name,
      workDate: r.work_date,
      description: r.description || undefined,
      photoBeforeUrl: r.photo_before_url || undefined,
      photoAfterUrl: r.photo_after_url || undefined,
      createdAt: r.created_at,
      updatedAt: r.updated_at,
      address: r.address || undefined
    }));
    res.json(mapped);
  } catch (err) {
    console.error('Error fetching work photos:', err);
    const msg = (err && err.message) || 'Failed to fetch work photos';
    res.status(500).json({ error: msg });
  }
});

// GET /api/pr/work-photos/:id - один фото отчёт по id
app.get(`${API_PREFIX}/pr/work-photos/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      `SELECT w.*, b.data->'passport'->>'address' as address
       FROM work_photos w
       LEFT JOIN buildings b ON w.building_id = b.id
       WHERE w.id = $1`,
      [id]
    );
    if (!rows || rows.length === 0) {
      return res.status(404).json({ error: 'Work photo not found' });
    }
    const r = rows[0];
    res.json({
      id: r.id,
      buildingId: r.building_id,
      residentReportId: r.resident_report_id || undefined,
      taskId: r.task_id || undefined,
      workName: r.work_name,
      workDate: r.work_date,
      description: r.description || undefined,
      photoBeforeUrl: r.photo_before_url || undefined,
      photoAfterUrl: r.photo_after_url || undefined,
      createdAt: r.created_at,
      updatedAt: r.updated_at,
      address: r.address || undefined
    });
  } catch (err) {
    console.error('Error fetching work photo:', err);
    res.status(500).json({ error: 'Failed to fetch work photo' });
  }
});

// POST /api/pr/work-photos - создание фото отчета (с загрузкой фото)
app.post(`${API_PREFIX}/pr/work-photos`, uploadWorkPhoto.fields([
  { name: 'photo_before', maxCount: 1 },
  { name: 'photo_after', maxCount: 1 }
]), async (req, res) => {
  try {
    const { building_id, resident_report_id, task_id, work_name, work_date, description } = req.body;

    if (!building_id || !work_name || !work_date) {
      // Удаляем загруженные файлы, если валидация не прошла
      if (req.files) {
        Object.values(req.files).flat().forEach(file => {
          fs.unlink(file.path, (err) => {
            if (err) console.error('Error deleting file:', err);
          });
        });
      }
      return res.status(400).json({ error: 'Missing required fields: building_id, work_name, work_date' });
    }

    const photoBeforeUrl = req.files?.photo_before?.[0] 
      ? `/uploads/work-photos/${req.files.photo_before[0].filename}` 
      : null;
    const photoAfterUrl = req.files?.photo_after?.[0] 
      ? `/uploads/work-photos/${req.files.photo_after[0].filename}` 
      : null;

    let result;
    try {
      result = await query(
        `INSERT INTO work_photos (building_id, resident_report_id, task_id, work_name, work_date, description, photo_before_url, photo_after_url)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
         RETURNING *`,
        [
          building_id,
          resident_report_id || null,
          task_id || null,
          work_name,
          work_date,
          description || null,
          photoBeforeUrl,
          photoAfterUrl
        ]
      );
    } catch (insertErr) {
      const msg = insertErr.message || '';
      if (msg.includes('task_id') && (msg.includes('does not exist') || msg.includes('column'))) {
        result = await query(
          `INSERT INTO work_photos (building_id, resident_report_id, work_name, work_date, description, photo_before_url, photo_after_url)
           VALUES ($1, $2, $3, $4, $5, $6, $7)
           RETURNING *`,
          [
            building_id,
            resident_report_id || null,
            work_name,
            work_date,
            description || null,
            photoBeforeUrl,
            photoAfterUrl
          ]
        );
      } else {
        throw insertErr;
      }
    }

    const row = result[0];
    if (!row) {
      return res.status(500).json({ error: 'Не удалось сохранить запись' });
    }
    res.status(201).json({
      id: row.id,
      buildingId: row.building_id,
      residentReportId: row.resident_report_id || undefined,
      taskId: row.task_id || undefined,
      workName: row.work_name,
      workDate: row.work_date,
      description: row.description || undefined,
      photoBeforeUrl: row.photo_before_url || undefined,
      photoAfterUrl: row.photo_after_url || undefined,
      createdAt: row.created_at,
      updatedAt: row.updated_at
    });
  } catch (err) {
    console.error('Error creating work photo:', err);
    const msg = err.message || 'Failed to create work photo';
    res.status(500).json({ error: msg });
  }
});

// PUT /api/pr/work-photos/:id - обновление фото отчета
app.put(`${API_PREFIX}/pr/work-photos/:id`, uploadWorkPhoto.fields([
  { name: 'photo_before', maxCount: 1 },
  { name: 'photo_after', maxCount: 1 }
]), async (req, res) => {
  try {
    const { id } = req.params;
    const { work_name, work_date, description, resident_report_id, task_id } = req.body;

    // Получаем текущую запись
    const currentResult = await query('SELECT * FROM work_photos WHERE id = $1', [id]);
    if (currentResult.length === 0) {
      // Удаляем загруженные файлы, если запись не найдена
      if (req.files) {
        Object.values(req.files).flat().forEach(file => {
          fs.unlink(file.path, (err) => {
            if (err) console.error('Error deleting file:', err);
          });
        });
      }
      return res.status(404).json({ error: 'Work photo not found' });
    }

    const current = currentResult[0];
    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (work_name !== undefined) {
      updates.push(`work_name = $${paramIndex++}`);
      params.push(work_name);
    }
    if (work_date !== undefined) {
      updates.push(`work_date = $${paramIndex++}`);
      params.push(work_date);
    }
    if (description !== undefined) {
      updates.push(`description = $${paramIndex++}`);
      params.push(description);
    }
    if (resident_report_id !== undefined) {
      updates.push(`resident_report_id = $${paramIndex++}`);
      params.push(resident_report_id);
    }
    if (task_id !== undefined) {
      updates.push(`task_id = $${paramIndex++}`);
      params.push(task_id);
    }

    // Обработка загруженных фото
    if (req.files?.photo_before?.[0]) {
      // Удаляем старое фото, если есть
      if (current.photo_before_url) {
        const oldPath = path.join(__dirname, current.photo_before_url);
        fs.unlink(oldPath, (err) => {
          if (err && err.code !== 'ENOENT') console.error('Error deleting old photo:', err);
        });
      }
      updates.push(`photo_before_url = $${paramIndex++}`);
      params.push(`/uploads/work-photos/${req.files.photo_before[0].filename}`);
    }

    if (req.files?.photo_after?.[0]) {
      // Удаляем старое фото, если есть
      if (current.photo_after_url) {
        const oldPath = path.join(__dirname, current.photo_after_url);
        fs.unlink(oldPath, (err) => {
          if (err && err.code !== 'ENOENT') console.error('Error deleting old photo:', err);
        });
      }
      updates.push(`photo_after_url = $${paramIndex++}`);
      params.push(`/uploads/work-photos/${req.files.photo_after[0].filename}`);
    }

    if (updates.length === 0) {
      // Удаляем загруженные файлы, если нет обновлений
      if (req.files) {
        Object.values(req.files).flat().forEach(file => {
          fs.unlink(file.path, (err) => {
            if (err) console.error('Error deleting file:', err);
          });
        });
      }
      return res.status(400).json({ error: 'No fields to update' });
    }

    updates.push(`updated_at = NOW()`);
    params.push(id);

    const result = await query(
      `UPDATE work_photos SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      params
    );

    const row = result[0];
    res.json({
      id: row.id,
      buildingId: row.building_id,
      residentReportId: row.resident_report_id || undefined,
      taskId: row.task_id || undefined,
      workName: row.work_name,
      workDate: row.work_date,
      description: row.description || undefined,
      photoBeforeUrl: row.photo_before_url || undefined,
      photoAfterUrl: row.photo_after_url || undefined,
      createdAt: row.created_at,
      updatedAt: row.updated_at
    });
  } catch (err) {
    console.error('Error updating work photo:', err);
    res.status(500).json({ error: 'Failed to update work photo' });
  }
});

// DELETE /api/pr/work-photos/:id - удаление фото отчета
app.delete(`${API_PREFIX}/pr/work-photos/:id`, async (req, res) => {
  try {
    const { id } = req.params;

    // Получаем запись для удаления файлов
    const result = await query('SELECT * FROM work_photos WHERE id = $1', [id]);
    if (result.length === 0) {
      return res.status(404).json({ error: 'Work photo not found' });
    }

    const workPhoto = result[0];

    // Удаляем файлы
    if (workPhoto.photo_before_url) {
      const beforePath = path.join(__dirname, workPhoto.photo_before_url);
      fs.unlink(beforePath, (err) => {
        if (err && err.code !== 'ENOENT') console.error('Error deleting photo:', err);
      });
    }
    if (workPhoto.photo_after_url) {
      const afterPath = path.join(__dirname, workPhoto.photo_after_url);
      fs.unlink(afterPath, (err) => {
        if (err && err.code !== 'ENOENT') console.error('Error deleting photo:', err);
      });
    }

    // Удаляем запись из БД
    await query('DELETE FROM work_photos WHERE id = $1', [id]);

    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting work photo:', err);
    res.status(500).json({ error: 'Failed to delete work photo' });
  }
});

// ========= PR МЕРОПРИЯТИЯ (pr_events) =========

// GET /api/pr/events - список мероприятий
app.get(`${API_PREFIX}/pr/events`, async (req, res) => {
  try {
    const { status, type, buildingId, districtId, from, to, limit = 50 } = req.query;
    let queryText = `
      SELECT id, title, date, type, category, status, location, location_type AS "locationType",
             location_building_id AS "locationBuildingId", location_place_type AS "locationPlaceType",
             location_district_id AS "locationDistrictId", location_building_ids AS "locationBuildingIds",
             attendees_count AS "attendeesCount", budget, short_plan AS "shortPlan", announcement, created_at AS "createdAt", created_by AS "createdBy"
      FROM pr_events
      WHERE 1=1
    `;
    const params = [];
    if (status) { queryText += ` AND status = $${params.length + 1}`; params.push(status); }
    if (type) { queryText += ` AND type = $${params.length + 1}`; params.push(type); }
    if (buildingId) { queryText += ` AND (location_building_id = $${params.length + 1} OR location_building_ids @> $${params.length + 2}::jsonb)`; params.push(buildingId, JSON.stringify([buildingId])); }
    if (districtId) { queryText += ` AND location_district_id = $${params.length + 1}`; params.push(districtId); }
    if (from) { queryText += ` AND date >= $${params.length + 1}`; params.push(from); }
    if (to) { queryText += ` AND date <= $${params.length + 1}`; params.push(to); }
    queryText += ` ORDER BY date DESC LIMIT $${params.length + 1}`;
    params.push(parseInt(limit) || 50);
    const rows = await query(queryText, params);
    const eventIds = rows.map(r => r.id);
    if (eventIds.length === 0) {
      return res.json(rows.map(r => ({ ...r, assignedEmployeeIds: [], photos: [] })));
    }
    const assignees = await query(
      `SELECT event_id, employee_id FROM pr_event_assignees WHERE event_id = ANY($1)`,
      [eventIds]
    );
    const assigneesByEvent = {};
    assignees.forEach(a => {
      if (!assigneesByEvent[a.event_id]) assigneesByEvent[a.event_id] = [];
      assigneesByEvent[a.event_id].push(a.employee_id);
    });
    const photos = await query(
      `SELECT id, event_id, photo_url AS "photoUrl", caption, location_type AS "locationType", location_building_id AS "locationBuildingId", created_at AS "createdAt"
       FROM pr_event_photos WHERE event_id = ANY($1) ORDER BY created_at`,
      [eventIds]
    );
    const photosByEvent = {};
    photos.forEach(p => {
      if (!photosByEvent[p.event_id]) photosByEvent[p.event_id] = [];
      photosByEvent[p.event_id].push(p);
    });
    const list = rows.map(r => ({
      ...r,
      assignedEmployeeIds: assigneesByEvent[r.id] || [],
      photos: photosByEvent[r.id] || []
    }));
    res.json(list);
  } catch (err) {
    if (err.code === '42P01') {
      return res.json([]);
    }
    console.error('Error fetching pr events:', err);
    res.status(500).json({ error: 'Failed to fetch events', details: err.message });
  }
});

// GET /api/pr/events/:id - одно мероприятие с assignees и фото
app.get(`${API_PREFIX}/pr/events/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      `SELECT id, title, date, type, category, status, location, location_type AS "locationType",
              location_building_id AS "locationBuildingId", location_place_type AS "locationPlaceType",
              location_district_id AS "locationDistrictId", location_building_ids AS "locationBuildingIds",
              attendees_count AS "attendeesCount", budget, short_plan AS "shortPlan", announcement, created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy"
       FROM pr_events WHERE id = $1`,
      [id]
    );
    if (!rows || rows.length === 0) {
      return res.status(404).json({ error: 'Event not found' });
    }
    const event = rows[0];
    const assignees = await query(
      `SELECT employee_id FROM pr_event_assignees WHERE event_id = $1`,
      [id]
    );
    const photos = await query(
      `SELECT id, event_id, photo_url AS "photoUrl", caption, location_type AS "locationType", location_building_id AS "locationBuildingId", created_at AS "createdAt"
       FROM pr_event_photos WHERE event_id = $1 ORDER BY created_at`,
      [id]
    );
    const invoiceRows = await query(
      `SELECT id, invoice_number AS "invoiceNumber", status, total_amount AS "totalAmount" FROM payment_invoices WHERE purpose_event_id = $1 ORDER BY created_at DESC`,
      [id]
    ).catch(() => []);
    res.json({
      ...event,
      assignedEmployeeIds: assignees.map(a => a.employee_id),
      photos: photos || [],
      invoices: invoiceRows || []
    });
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Event not found' });
    console.error('Error fetching pr event:', err);
    res.status(500).json({ error: 'Failed to fetch event', details: err.message });
  }
});

// POST /api/pr/events - создание мероприятия
app.post(`${API_PREFIX}/pr/events`, async (req, res) => {
  try {
    const {
      title, date, type, category, status = 'planned', location, locationType, locationBuildingId,
      locationPlaceType, locationDistrictId, locationBuildingIds = [],
      attendeesCount = 0, budget, shortPlan, announcement, createdBy, assignedEmployeeIds = []
    } = req.body;
    if (!title || !date || !type || !category) {
      return res.status(400).json({ error: 'Missing required fields: title, date, type, category' });
    }
    const buildingIdsJson = Array.isArray(locationBuildingIds) ? JSON.stringify(locationBuildingIds) : '[]';
    const result = await query(
      `INSERT INTO pr_events (title, date, type, category, status, location, location_type, location_building_id, location_place_type, location_district_id, location_building_ids, attendees_count, budget, short_plan, announcement, created_by, updated_at)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12, $13, $14, $15, $16, NOW())
       RETURNING id, title, date, type, category, status, location, location_type AS "locationType", location_building_id AS "locationBuildingId",
                 location_place_type AS "locationPlaceType", location_district_id AS "locationDistrictId", location_building_ids AS "locationBuildingIds",
                 attendees_count AS "attendeesCount", budget, short_plan AS "shortPlan", announcement, created_at AS "createdAt", created_by AS "createdBy"`,
      [title, date, type, category, status, location || null, locationType || null, locationBuildingId || null, locationPlaceType || null, locationDistrictId || null, buildingIdsJson, parseInt(attendeesCount) || 0, budget ? parseFloat(budget) : null, shortPlan || null, announcement || null, createdBy || null]
    ).catch(async (err) => {
      if (err.message && (err.message.includes('location_place_type') || err.message.includes('location_district_id') || err.message.includes('location_building_ids'))) {
        return await query(
          `INSERT INTO pr_events (title, date, type, category, status, location, location_type, location_building_id, attendees_count, budget, short_plan, announcement, created_by, updated_at)
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW())
           RETURNING id, title, date, type, category, status, location, location_type AS "locationType", location_building_id AS "locationBuildingId",
                     attendees_count AS "attendeesCount", budget, short_plan AS "shortPlan", announcement, created_at AS "createdAt", created_by AS "createdBy"`,
          [title, date, type, category, status, location || null, locationType || null, locationBuildingId || null, parseInt(attendeesCount) || 0, budget ? parseFloat(budget) : null, shortPlan || null, announcement || null, createdBy || null]
        );
      }
      throw err;
    });
    const event = result[0];
    if (Array.isArray(assignedEmployeeIds) && assignedEmployeeIds.length > 0) {
      for (const empId of assignedEmployeeIds) {
        if (empId) await query(`INSERT INTO pr_event_assignees (event_id, employee_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [event.id, empId]);
      }
    }
    const assignees = await query(`SELECT employee_id FROM pr_event_assignees WHERE event_id = $1`, [event.id]);
    try {
      const empIds = assignees.map(a => a.employee_id).filter(Boolean);
      const userIds = empIds.length > 0 ? await notificationService.resolveEmployeeIdsToUserIds(pool, empIds) : [];
      const opts = {
        type: 'pr_event',
        title: 'Новое мероприятие',
        body: (event.title || '').toString().slice(0, 150),
        entityType: 'pr_event',
        entityId: String(event.id),
      };
      if (userIds.length > 0) {
        await notificationService.createNotificationForUserIds(pool, userIds, opts);
      } else {
        await notificationService.createNotificationForResponsibleZone(pool, 'pr', 'events', opts);
      }
    } catch (notifErr) {
      console.warn('Notification (pr event create):', notifErr.message);
    }
    res.status(201).json({
      ...event,
      assignedEmployeeIds: assignees.map(a => a.employee_id),
      photos: []
    });
  } catch (err) {
    if (err.code === '42P01') return res.status(503).json({ error: 'PR events table not ready' });
    console.error('Error creating pr event:', err);
    res.status(500).json({ error: 'Failed to create event', details: err.message });
  }
});

// PUT /api/pr/events/:id - обновление мероприятия
app.put(`${API_PREFIX}/pr/events/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const {
      title, date, type, category, status, location, locationType, locationBuildingId,
      locationPlaceType, locationDistrictId, locationBuildingIds,
      attendeesCount, budget, shortPlan, announcement, assignedEmployeeIds
    } = req.body;
    const existing = await query('SELECT id FROM pr_events WHERE id = $1', [id]);
    if (!existing || existing.length === 0) {
      return res.status(404).json({ error: 'Event not found' });
    }
    const updates = [];
    const params = [];
    let idx = 1;
    if (title !== undefined) { updates.push(`title = $${idx++}`); params.push(title); }
    if (date !== undefined) { updates.push(`date = $${idx++}`); params.push(date); }
    if (type !== undefined) { updates.push(`type = $${idx++}`); params.push(type); }
    if (category !== undefined) { updates.push(`category = $${idx++}`); params.push(category); }
    if (status !== undefined) { updates.push(`status = $${idx++}`); params.push(status); }
    if (location !== undefined) { updates.push(`location = $${idx++}`); params.push(location); }
    if (locationType !== undefined) { updates.push(`location_type = $${idx++}`); params.push(locationType); }
    if (locationBuildingId !== undefined) { updates.push(`location_building_id = $${idx++}`); params.push(locationBuildingId); }
    if (locationPlaceType !== undefined) { updates.push(`location_place_type = $${idx++}`); params.push(locationPlaceType); }
    if (locationDistrictId !== undefined) { updates.push(`location_district_id = $${idx++}`); params.push(locationDistrictId); }
    if (locationBuildingIds !== undefined) { updates.push(`location_building_ids = $${idx++}`); params.push(JSON.stringify(Array.isArray(locationBuildingIds) ? locationBuildingIds : [])); }
    if (attendeesCount !== undefined) { updates.push(`attendees_count = $${idx++}`); params.push(parseInt(attendeesCount)); }
    if (budget !== undefined) { updates.push(`budget = $${idx++}`); params.push(budget ? parseFloat(budget) : null); }
    if (shortPlan !== undefined) { updates.push(`short_plan = $${idx++}`); params.push(shortPlan); }
    if (announcement !== undefined) { updates.push(`announcement = $${idx++}`); params.push(announcement); }
    updates.push('updated_at = NOW()');
    params.push(id);
    await query(
      `UPDATE pr_events SET ${updates.join(', ')} WHERE id = $${idx}`,
      params
    );
    if (Array.isArray(assignedEmployeeIds)) {
      await query('DELETE FROM pr_event_assignees WHERE event_id = $1', [id]);
      for (const empId of assignedEmployeeIds) {
        if (empId) await query(`INSERT INTO pr_event_assignees (event_id, employee_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [id, empId]);
      }
    }
    const rows = await query(
      `SELECT id, title, date, type, category, status, location, location_type AS "locationType", location_building_id AS "locationBuildingId",
              location_place_type AS "locationPlaceType", location_district_id AS "locationDistrictId", location_building_ids AS "locationBuildingIds",
              attendees_count AS "attendeesCount", budget, short_plan AS "shortPlan", announcement, created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy"
       FROM pr_events WHERE id = $1`,
      [id]
    );
    const assignees = await query(`SELECT employee_id FROM pr_event_assignees WHERE event_id = $1`, [id]);
    const photos = await query(
      `SELECT id, event_id, photo_url AS "photoUrl", caption, location_type AS "locationType", location_building_id AS "locationBuildingId", created_at AS "createdAt"
       FROM pr_event_photos WHERE event_id = $1 ORDER BY created_at`,
      [id]
    );
    try {
      const empIds = assignees.map(a => a.employee_id).filter(Boolean);
      const userIds = empIds.length > 0 ? await notificationService.resolveEmployeeIdsToUserIds(pool, empIds) : [];
      const opts = {
        type: 'pr_event',
        title: 'Изменение мероприятия',
        body: (rows[0].title || '').toString().slice(0, 150),
        entityType: 'pr_event',
        entityId: String(id),
      };
      if (userIds.length > 0) {
        await notificationService.createNotificationForUserIds(pool, userIds, opts);
      } else {
        await notificationService.createNotificationForResponsibleZone(pool, 'pr', 'events', opts);
      }
    } catch (notifErr) {
      console.warn('Notification (pr event update):', notifErr.message);
    }
    res.json({
      ...rows[0],
      assignedEmployeeIds: assignees.map(a => a.employee_id),
      photos: photos || []
    });
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Event not found' });
    console.error('Error updating pr event:', err);
    res.status(500).json({ error: 'Failed to update event', details: err.message });
  }
});

// DELETE /api/pr/events/:id
app.delete(`${API_PREFIX}/pr/events/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const existing = await query('SELECT id FROM pr_events WHERE id = $1', [id]);
    if (!existing || existing.length === 0) {
      return res.status(404).json({ error: 'Event not found' });
    }
    await query('DELETE FROM pr_events WHERE id = $1', [id]);
    res.json({ success: true });
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Event not found' });
    console.error('Error deleting pr event:', err);
    res.status(500).json({ error: 'Failed to delete event', details: err.message });
  }
});

// POST /api/pr/events/:id/photos - загрузка фото мероприятия
app.post(`${API_PREFIX}/pr/events/:id/photos`, uploadEventPhoto.single('photo'), async (req, res) => {
  try {
    const { id } = req.params;
    const { caption, locationType, locationBuildingId } = req.body || {};
    const existing = await query('SELECT id FROM pr_events WHERE id = $1', [id]);
    if (!existing || existing.length === 0) {
      if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
      return res.status(404).json({ error: 'Event not found' });
    }
    if (!req.file) {
      return res.status(400).json({ error: 'No photo file' });
    }
    const photoUrl = `/api/pr/events/${id}/photos/file/${req.file.filename}`;
    await query(
      `INSERT INTO pr_event_photos (event_id, photo_url, caption, location_type, location_building_id)
       VALUES ($1, $2, $3, $4, $5)
       RETURNING id, event_id, photo_url AS "photoUrl", caption, location_type AS "locationType", location_building_id AS "locationBuildingId", created_at AS "createdAt"`,
      [id, photoUrl, caption || null, locationType || null, locationBuildingId || null]
    );
    const result = await query(
      `SELECT id, event_id, photo_url AS "photoUrl", caption, location_type AS "locationType", location_building_id AS "locationBuildingId", created_at AS "createdAt"
       FROM pr_event_photos WHERE event_id = $1 ORDER BY created_at DESC LIMIT 1`,
      [id]
    );
    res.status(201).json(result[0]);
  } catch (err) {
    if (req.file && req.file.path && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
    if (err.code === '42P01') return res.status(404).json({ error: 'Event not found' });
    console.error('Error uploading event photo:', err);
    res.status(500).json({ error: 'Failed to upload photo', details: err.message });
  }
});

// GET /api/pr/scheduled-posts/image/:filename - отдача файла изображения поста
app.get(`${API_PREFIX}/pr/scheduled-posts/image/:filename`, (req, res) => {
  try {
    const { filename } = req.params;
    const filePath = path.join(postImagesDir, filename);
    if (!fs.existsSync(filePath)) {
      return res.status(404).json({ error: 'Image not found' });
    }
    res.sendFile(filePath);
  } catch (err) {
    console.error('Error serving post image:', err);
    res.status(500).json({ error: 'Failed to serve image' });
  }
});

// GET /api/pr/events/:id/photos/file/:filename - отдача файла фото мероприятия
app.get(`${API_PREFIX}/pr/events/:id/photos/file/:filename`, (req, res) => {
  try {
    const filename = req.params.filename;
    const filePath = path.join(eventPhotosDir, filename);
    if (!path.resolve(filePath).startsWith(path.resolve(eventPhotosDir)) || !fs.existsSync(filePath)) {
      return res.status(404).json({ error: 'File not found' });
    }
    res.sendFile(filePath);
  } catch (err) {
    res.status(500).json({ error: 'Error serving file' });
  }
});

// GET /api/pr/events/:id/photos - список фото мероприятия
app.get(`${API_PREFIX}/pr/events/:id/photos`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      `SELECT id, event_id, photo_url AS "photoUrl", caption, location_type AS "locationType", location_building_id AS "locationBuildingId", created_at AS "createdAt"
       FROM pr_event_photos WHERE event_id = $1 ORDER BY created_at`,
      [id]
    );
    res.json(rows || []);
  } catch (err) {
    if (err.code === '42P01') return res.json([]);
    console.error('Error fetching event photos:', err);
    res.status(500).json({ error: 'Failed to fetch photos', details: err.message });
  }
});

// DELETE /api/pr/events/:id/photos/:photoId
app.delete(`${API_PREFIX}/pr/events/:id/photos/:photoId`, async (req, res) => {
  try {
    const { id, photoId } = req.params;
    const row = await query('SELECT id, photo_url FROM pr_event_photos WHERE id = $1 AND event_id = $2', [photoId, id]);
    if (!row || row.length === 0) return res.status(404).json({ error: 'Photo not found' });
    const photoUrl = row[0].photo_url;
    const match = photoUrl && photoUrl.match(/\/file\/([^/]+)$/);
    if (match) {
      const filePath = path.join(eventPhotosDir, match[1]);
      if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
    }
    await query('DELETE FROM pr_event_photos WHERE id = $1', [photoId]);
    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting event photo:', err);
    res.status(500).json({ error: 'Failed to delete photo', details: err.message });
  }
});

// ========= НОВОСТИ КОМПАНИИ (company_news) =========
// Маппинг отделов (frontend Department) в разделы для уведомлений
const DEPARTMENT_TO_SECTION = { production: 'dashboard', pr: 'pr', finance: 'finance', development: 'development', legal: 'legal', hr: 'hr' };

function mapNotifyDepartmentsToSections(notifyDepartments) {
  if (!Array.isArray(notifyDepartments) || notifyDepartments.length === 0) return [];
  const sections = new Set();
  for (const d of notifyDepartments) {
    if (DEPARTMENT_TO_SECTION[d]) sections.add(DEPARTMENT_TO_SECTION[d]);
  }
  return Array.from(sections);
}

async function sendCompanyNewsNotifications(newsId, title, body, notifyDepartments, notifyEmployeeIds) {
  try {
    const sections = mapNotifyDepartmentsToSections(notifyDepartments || []);
    const userIdsBySection = sections.length ? await notificationService.getPortalUserIdsBySections(pool, sections) : [];
    const userIdsByEmployees = (notifyEmployeeIds && notifyEmployeeIds.length)
      ? await notificationService.resolveEmployeeIdsToUserIds(pool, notifyEmployeeIds)
      : [];
    const allUserIds = [...new Set([...userIdsBySection, ...userIdsByEmployees])];
    if (allUserIds.length === 0) return 0;
    const shortBody = body && body.length > 80 ? body.slice(0, 80) + '…' : (body || null);
    return await notificationService.createNotificationForUserIds(pool, allUserIds, {
      type: 'company_news',
      title: title || 'Новость компании',
      body: shortBody,
      entityType: 'company_news',
      entityId: String(newsId),
      payload: { newsId }
    });
  } catch (notifErr) {
    console.warn('[company-news] notifications on publish:', notifErr.message);
    return 0;
  }
}

// GET /api/company-news — список (query: status, limit, offset)
app.get(`${API_PREFIX}/company-news`, async (req, res) => {
  try {
    const status = req.query.status ? String(req.query.status).trim() : null;
    const limit = Math.min(parseInt(req.query.limit, 10) || 50, 100);
    const offset = parseInt(req.query.offset, 10) || 0;
    let where = '';
    const params = [];
    if (status && ['draft', 'pending', 'published'].includes(status)) {
      params.push(status);
      where = ' WHERE cn.status = $1';
    }
    const countResult = await query(
      `SELECT COUNT(*)::int AS c FROM company_news cn${where}`,
      params
    );
    const total = countResult[0].c;
    params.push(limit, offset);
    const orderCol = status === 'published' ? 'published_at' : 'created_at';
    const rows = await query(
      `SELECT cn.id, cn.title, cn.body, cn.status, cn.created_at AS "createdAt", cn.updated_at AS "updatedAt",
              cn.published_at AS "publishedAt", cn.notify_departments AS "notifyDepartments", cn.notify_employee_ids AS "notifyEmployeeIds",
              cn.created_by AS "createdBy", e.name AS "createdByName"
       FROM company_news cn
       LEFT JOIN portal_users pu ON pu.id = cn.created_by
       LEFT JOIN employees e ON e.id = pu.employee_id
       ${where} ORDER BY cn.${orderCol} DESC NULLS LAST LIMIT $${params.length - 1} OFFSET $${params.length}`,
      params
    );
    res.setHeader('X-Total-Count', String(total));
    res.json(rows);
  } catch (err) {
    if (err.code === '42P01') return res.json([]);
    console.error('Error GET /company-news:', err);
    res.status(500).json({ error: 'Failed to list company news', details: err.message });
  }
});

// GET /api/company-news/:id — одна новость
app.get(`${API_PREFIX}/company-news/:id`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const rows = await query(
      `SELECT cn.id, cn.title, cn.body, cn.status, cn.created_at AS "createdAt", cn.updated_at AS "updatedAt",
              cn.published_at AS "publishedAt", cn.notify_departments AS "notifyDepartments", cn.notify_employee_ids AS "notifyEmployeeIds",
              cn.created_by AS "createdBy", e.name AS "createdByName"
       FROM company_news cn
       LEFT JOIN portal_users pu ON pu.id = cn.created_by
       LEFT JOIN employees e ON e.id = pu.employee_id
       WHERE cn.id = $1`,
      [id]
    );
    if (!rows || rows.length === 0) return res.status(404).json({ error: 'News not found' });
    res.json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'News not found' });
    console.error('Error GET /company-news/:id:', err);
    res.status(500).json({ error: 'Failed to get news', details: err.message });
  }
});

// POST /api/company-news — создание (требует auth)
app.post(`${API_PREFIX}/company-news`, authMiddleware, async (req, res) => {
  try {
    const { title, body, status, notifyDepartments, notifyEmployeeIds } = req.body || {};
    if (!title || !String(title).trim()) return res.status(400).json({ error: 'Title is required' });
    const st = (status && ['draft', 'pending', 'published'].includes(status)) ? status : 'draft';
    const createdBy = req.user && req.user.userId ? req.user.userId : null;
    const notifyDept = Array.isArray(notifyDepartments) ? notifyDepartments : [];
    const notifyEmp = Array.isArray(notifyEmployeeIds) ? notifyEmployeeIds : [];
    const rows = await query(
      `INSERT INTO company_news (title, body, status, created_by, updated_at, notify_departments, notify_employee_ids)
       VALUES ($1, $2, $3, $4, NOW(), $5::jsonb, $6::jsonb)
       RETURNING id, title, body, status, created_at AS "createdAt", updated_at AS "updatedAt", published_at AS "publishedAt",
                 notify_departments AS "notifyDepartments", notify_employee_ids AS "notifyEmployeeIds", created_by AS "createdBy"`,
      [String(title).trim(), body != null ? String(body) : null, st, createdBy, JSON.stringify(notifyDept), JSON.stringify(notifyEmp)]
    );
    const row = rows[0];
    if (st === 'published') {
      await query('UPDATE company_news SET published_at = NOW() WHERE id = $1', [row.id]);
      row.publishedAt = new Date().toISOString();
      await sendCompanyNewsNotifications(row.id, row.title, row.body, notifyDept, notifyEmp);
    }
    res.status(201).json(row);
  } catch (err) {
    if (err.code === '42P01') return res.status(503).json({ error: 'Таблица новостей ещё не создана. Перезапустите сервер.' });
    console.error('Error POST /company-news:', err);
    res.status(500).json({ error: 'Failed to create company news', details: err.message });
  }
});

// PUT /api/company-news/:id — обновление (в т.ч. смена статуса на published)
app.put(`${API_PREFIX}/company-news/:id`, authMiddleware, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const { title, body, status, notifyDepartments, notifyEmployeeIds } = req.body || {};
    const existing = await query('SELECT id, status, title, body, notify_departments, notify_employee_ids FROM company_news WHERE id = $1', [id]);
    if (!existing || existing.length === 0) return res.status(404).json({ error: 'News not found' });
    const cur = existing[0];
    const newTitle = title !== undefined ? String(title).trim() : cur.title;
    const newBody = body !== undefined ? (body != null ? String(body) : null) : cur.body;
    const newStatus = (status && ['draft', 'pending', 'published'].includes(status)) ? status : cur.status;
    const newNotifyDept = notifyDepartments !== undefined ? (Array.isArray(notifyDepartments) ? notifyDepartments : []) : cur.notify_departments;
    const newNotifyEmp = notifyEmployeeIds !== undefined ? (Array.isArray(notifyEmployeeIds) ? notifyEmployeeIds : []) : cur.notify_employee_ids;
    const updates = ['title = $1', 'body = $2', 'status = $3', 'updated_at = NOW()', 'notify_departments = $4', 'notify_employee_ids = $5'];
    const values = [newTitle, newBody, newStatus, JSON.stringify(newNotifyDept), JSON.stringify(newNotifyEmp)];
    if (newStatus === 'published' && cur.status !== 'published') {
      updates.push('published_at = NOW()');
      values.push(null);
    }
    values.push(id);
    await query(
      `UPDATE company_news SET ${updates.join(', ')} WHERE id = $${values.length}`,
      values
    );
    if (newStatus === 'published' && cur.status !== 'published') {
      await sendCompanyNewsNotifications(id, newTitle, newBody, newNotifyDept, newNotifyEmp);
    }
    const rows = await query(
      `SELECT cn.id, cn.title, cn.body, cn.status, cn.created_at AS "createdAt", cn.updated_at AS "updatedAt",
              cn.published_at AS "publishedAt", cn.notify_departments AS "notifyDepartments", cn.notify_employee_ids AS "notifyEmployeeIds",
              cn.created_by AS "createdBy", e.name AS "createdByName"
       FROM company_news cn
       LEFT JOIN portal_users pu ON pu.id = cn.created_by
       LEFT JOIN employees e ON e.id = pu.employee_id
       WHERE cn.id = $1`,
      [id]
    );
    res.json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'News not found' });
    console.error('Error PUT /company-news/:id:', err);
    res.status(500).json({ error: 'Failed to update company news', details: err.message });
  }
});

// DELETE /api/company-news/:id
app.delete(`${API_PREFIX}/company-news/:id`, authMiddleware, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const result = await query('DELETE FROM company_news WHERE id = $1 RETURNING id', [id]);
    if (result.length === 0) return res.status(404).json({ error: 'News not found' });
    res.json({ success: true });
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'News not found' });
    console.error('Error DELETE /company-news/:id:', err);
    res.status(500).json({ error: 'Failed to delete company news', details: err.message });
  }
});

// ========= PR SMM КАНАЛЫ И СНИМКИ ПОДПИСЧИКОВ =========

// GET /api/pr/smm-channels/summary — сводка по подписчикам (маршрут до /:id)
app.get(`${API_PREFIX}/pr/smm-channels/summary`, async (req, res) => {
  try {
    const channels = await query(
      `SELECT id, name, type FROM pr_smm_channels ORDER BY sort_order ASC, id ASC`
    );
    if (channels.length === 0) return res.json({ total: 0, byChannel: [] });
    const channelIds = channels.map(c => c.id);
    const latestSnapshots = await query(
      `SELECT DISTINCT ON (channel_id) channel_id AS "channelId", subscribers_count AS "subscribersCount"
       FROM pr_smm_subscriber_snapshots WHERE channel_id = ANY($1) ORDER BY channel_id, recorded_at DESC`,
      [channelIds]
    );
    const snapshotByChannel = {};
    latestSnapshots.forEach(s => { snapshotByChannel[s.channelId] = s.subscribersCount; });
    const byChannel = channels.map(c => ({
      id: c.id,
      name: c.name,
      type: c.type,
      subscribersCount: snapshotByChannel[c.id] ?? 0
    }));
    const total = byChannel.reduce((sum, c) => sum + (c.subscribersCount || 0), 0);
    res.json({ total, byChannel });
  } catch (err) {
    if (err.code === '42P01') return res.json({ total: 0, byChannel: [] });
    console.error('Error GET /pr/smm-channels/summary:', err);
    res.status(500).json({ error: 'Failed to get SMM summary', details: err.message });
  }
});

// GET /api/pr/dashboard — полный дашборд PR для продвинутой статистики и карточки сводки
app.get(`${API_PREFIX}/pr/dashboard`, async (req, res) => {
  const now = new Date();
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
  const monthStartStr = monthStart.toISOString().slice(0, 10);
  const todayStr = now.toISOString().slice(0, 10);

  const defaultData = {
    npsCompany: 0,
    npsResponsesCount: 0,
    npsAvgScore: 0,
    npsPromoters: 0,
    npsPassives: 0,
    npsDetractors: 0,
    csatPercent: 0,
    reviewsAvgRating: 0,
    reviewsTotal: 0,
    smm: { total: 0, byChannel: [] },
    reviewsStats: { total: 0, new_count: 0, processed_count: 0, archived_count: 0, positive_count: 0, negative_count: 0, yandex_count: 0, gis2_count: 0 },
    incidentsSummary: { total: 0, open: 0, in_progress: 0, resolved: 0 },
    events: { countMonth: 0, upcoming: 0, past: 0 },
    residentReports: { total: 0, publishedThisMonth: 0 },
    workPhotos: { total: 0, thisMonth: 0 },
    npsSurveys: { total: 0, active: 0, list: [] },
    scheduledPosts: { draft: 0, pending_approval: 0, approved: 0, published: 0, rejected: 0, edited: 0 },
    negative: { openIncidents: 0, negativeReviewsCount: 0 }
  };

  try {
    // 1. SMM — переиспользуем логику summary
    let smm = { total: 0, byChannel: [] };
    try {
      const channels = await query('SELECT id, name, type FROM pr_smm_channels ORDER BY sort_order ASC, id ASC');
      if (channels.length > 0) {
        const channelIds = channels.map(c => c.id);
        const latestSnapshots = await query(
          `SELECT DISTINCT ON (channel_id) channel_id AS "channelId", subscribers_count AS "subscribersCount"
           FROM pr_smm_subscriber_snapshots WHERE channel_id = ANY($1) ORDER BY channel_id, recorded_at DESC`,
          [channelIds]
        );
        const snapshotByChannel = {};
        latestSnapshots.forEach(s => { snapshotByChannel[s.channelId] = s.subscribersCount; });
        smm.byChannel = channels.map(c => ({
          id: c.id,
          name: c.name,
          type: c.type,
          subscribersCount: snapshotByChannel[c.id] ?? 0
        }));
        smm.total = smm.byChannel.reduce((sum, c) => sum + (c.subscribersCount || 0), 0);
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 2. NPS по компании — все ответы за текущий месяц
    let npsCompany = 0, npsResponsesCount = 0, npsAvgScore = 0, npsPromoters = 0, npsPassives = 0, npsDetractors = 0;
    try {
      const npsRes = await query(`
        SELECT
          COUNT(*) as total,
          AVG(score)::numeric(4,2) as avg_score,
          COUNT(*) FILTER (WHERE score >= 9) as promoters,
          COUNT(*) FILTER (WHERE score >= 7 AND score <= 8) as passives,
          COUNT(*) FILTER (WHERE score <= 6) as detractors
        FROM nps_responses
        WHERE created_at >= $1
      `, [monthStart]);
      if (npsRes.length > 0 && parseInt(npsRes[0].total) > 0) {
        const r = npsRes[0];
        npsResponsesCount = parseInt(r.total) || 0;
        npsAvgScore = parseFloat(r.avg_score) || 0;
        npsPromoters = parseInt(r.promoters) || 0;
        npsPassives = parseInt(r.passives) || 0;
        npsDetractors = parseInt(r.detractors) || 0;
        npsCompany = Math.round((npsPromoters / npsResponsesCount * 100) - (npsDetractors / npsResponsesCount * 100));
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 3. Отзывы — stats и CSAT (rating >= 7 как положительные)
    let reviewsStats = defaultData.reviewsStats;
    let csatPercent = 0, reviewsAvgRating = 0, reviewsTotal = 0;
    try {
      const revStats = await query(`
        SELECT
          COUNT(*) as total,
          COUNT(*) FILTER (WHERE status = 'new') as new_count,
          COUNT(*) FILTER (WHERE status = 'processed') as processed_count,
          COUNT(*) FILTER (WHERE status = 'archived') as archived_count,
          AVG(rating)::numeric(4,2) as avg_rating,
          COUNT(*) FILTER (WHERE rating >= 7) as positive_count,
          COUNT(*) FILTER (WHERE rating <= 3) as negative_count,
          COUNT(*) FILTER (WHERE source = 'yandex_maps') as yandex_count,
          COUNT(*) FILTER (WHERE source = '2gis') as gis2_count
        FROM reviews
        WHERE 1=1
      `);
      if (revStats.length > 0) {
        const r = revStats[0];
        reviewsTotal = parseInt(r.total) || 0;
        reviewsStats = {
          total: reviewsTotal,
          new_count: parseInt(r.new_count) || 0,
          processed_count: parseInt(r.processed_count) || 0,
          archived_count: parseInt(r.archived_count) || 0,
          positive_count: parseInt(r.positive_count) || 0,
          negative_count: parseInt(r.negative_count) || 0,
          yandex_count: parseInt(r.yandex_count) || 0,
          gis2_count: parseInt(r.gis2_count) || 0
        };
        reviewsAvgRating = parseFloat(r.avg_rating) || 0;
        csatPercent = reviewsTotal > 0 ? Math.round((parseInt(r.positive_count) || 0) / reviewsTotal * 100) : 0;
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 4. Инциденты — по статусам
    let incidentsSummary = defaultData.incidentsSummary;
    try {
      const incRes = await query(`
        SELECT
          COUNT(*) as total,
          COUNT(*) FILTER (WHERE status = 'open') as open,
          COUNT(*) FILTER (WHERE status = 'in_progress') as in_progress,
          COUNT(*) FILTER (WHERE status = 'resolved') as resolved
        FROM incidents
      `);
      if (incRes.length > 0) {
        const r = incRes[0];
        incidentsSummary = {
          total: parseInt(r.total) || 0,
          open: parseInt(r.open) || 0,
          in_progress: parseInt(r.in_progress) || 0,
          resolved: parseInt(r.resolved) || 0
        };
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 5. Мероприятия — за месяц, предстоящие, прошедшие
    let events = defaultData.events;
    try {
      const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
      const monthEndStr = monthEnd.toISOString().slice(0, 10);
      const evRes = await query(`
        SELECT
          COUNT(*) FILTER (WHERE date >= $1 AND date <= $2) as count_month,
          COUNT(*) FILTER (WHERE date > $3) as upcoming,
          COUNT(*) FILTER (WHERE date < $1) as past
        FROM pr_events
        WHERE 1=1
      `, [monthStartStr, monthEndStr, todayStr]);
      if (evRes.length > 0) {
        const r = evRes[0];
        events = {
          countMonth: parseInt(r.count_month) || 0,
          upcoming: parseInt(r.upcoming) || 0,
          past: parseInt(r.past) || 0
        };
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 6. Отчёты жителям
    let residentReports = defaultData.residentReports;
    try {
      const rrRes = await query(`
        SELECT
          COUNT(*) as total,
          COUNT(*) FILTER (WHERE status = 'published' AND (published_at::date >= $1 OR updated_at::date >= $1)) as published_this_month
        FROM resident_reports
      `, [monthStartStr]);
      if (rrRes.length > 0) {
        const r = rrRes[0];
        residentReports = {
          total: parseInt(r.total) || 0,
          publishedThisMonth: parseInt(r.published_this_month) || 0
        };
      }
    } catch (e) {
      try {
        const rrTotal = await query('SELECT COUNT(*) as c FROM resident_reports');
        residentReports = { total: parseInt(rrTotal[0]?.c) || 0, publishedThisMonth: 0 };
      } catch (e2) { if (e2.code !== '42P01') throw e; }
    }

    // 7. Фото отчёты — всего и за месяц (по work_date или created_at)
    let workPhotos = defaultData.workPhotos;
    try {
      const wpRes = await query(`
        SELECT
          COUNT(*) as total,
          COUNT(*) FILTER (WHERE (work_date >= $1 OR (work_date IS NULL AND created_at >= $1))) as this_month
        FROM work_photos
      `, [monthStartStr]);
      if (wpRes.length > 0) {
        const r = wpRes[0];
        workPhotos = { total: parseInt(r.total) || 0, thisMonth: parseInt(r.this_month) || 0 };
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 8. NPS опросы — всего, активные, список с кол-вом ответов (топ-5)
    let npsSurveys = defaultData.npsSurveys;
    try {
      const countRes = await query(`SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE status = 'active') as active FROM nps_surveys`);
      const surveysRes = await query(`
        SELECT s.id, s.title, s.status,
               (SELECT COUNT(*) FROM nps_responses r WHERE r.survey_id = s.id) as responses_count
        FROM nps_surveys s
        ORDER BY s.created_at DESC
        LIMIT 10
      `);
      const totalSurveys = countRes.length > 0 ? parseInt(countRes[0].total) || 0 : 0;
      const activeSurveys = countRes.length > 0 ? parseInt(countRes[0].active) || 0 : 0;
      npsSurveys = {
        total: totalSurveys,
        active: activeSurveys,
        list: (surveysRes || []).slice(0, 5).map(s => ({
          id: s.id,
          title: s.title || 'Без названия',
          responsesCount: parseInt(s.responses_count) || 0,
          status: s.status
        }))
      };
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 9. Отложенные посты — по статусам
    let scheduledPosts = defaultData.scheduledPosts;
    try {
      const spRes = await query(`
        SELECT status, COUNT(*) as cnt
        FROM pr_scheduled_posts
        GROUP BY status
      `);
      if (spRes.length > 0) {
        const map = { draft: 0, pending_approval: 0, approved: 0, published: 0, rejected: 0, edited: 0 };
        spRes.forEach(row => {
          const k = row.status in map ? row.status : 'draft';
          map[k] = parseInt(row.cnt) || 0;
        });
        scheduledPosts = { ...map };
      }
    } catch (e) { if (e.code !== '42P01') throw e; }

    // 10. Негатив в работе — открытые инциденты и кол-во негативных отзывов
    const negative = {
      openIncidents: incidentsSummary.open + incidentsSummary.in_progress,
      negativeReviewsCount: reviewsStats.negative_count
    };

    res.json({
      success: true,
      data: {
        npsCompany,
        npsResponsesCount,
        npsAvgScore,
        npsPromoters,
        npsPassives,
        npsDetractors,
        csatPercent,
        reviewsAvgRating,
        reviewsTotal,
        smm,
        reviewsStats,
        incidentsSummary,
        events,
        residentReports,
        workPhotos,
        npsSurveys,
        scheduledPosts,
        negative
      }
    });
  } catch (err) {
    console.error('[pr/dashboard] Ошибка:', err);
    res.status(500).json({ success: false, error: err.message || 'Failed to get PR dashboard' });
  }
});

// GET /api/pr/smm-channels — список каналов с последним снимком подписчиков
app.get(`${API_PREFIX}/pr/smm-channels`, async (req, res) => {
  try {
    const channels = await query(
      `SELECT id, name, type, url, sort_order AS "sortOrder", created_at AS "createdAt"
       FROM pr_smm_channels ORDER BY sort_order ASC, id ASC`
    );
    if (channels.length === 0) return res.json([]);
    const channelIds = channels.map(c => c.id);
    const latestSnapshots = await query(
      `SELECT DISTINCT ON (channel_id) channel_id AS "channelId", subscribers_count AS "subscribersCount", recorded_at AS "recordedAt", note, created_at AS "createdAt"
       FROM pr_smm_subscriber_snapshots WHERE channel_id = ANY($1) ORDER BY channel_id, recorded_at DESC`,
      [channelIds]
    );
    const snapshotByChannel = {};
    latestSnapshots.forEach(s => { snapshotByChannel[s.channelId] = s; });
    const list = channels.map(c => ({
      ...c,
      lastSnapshot: snapshotByChannel[c.id] || null,
      subscribersCount: snapshotByChannel[c.id] ? snapshotByChannel[c.id].subscribersCount : null
    }));
    res.json(list);
  } catch (err) {
    if (err.code === '42P01') return res.json([]);
    console.error('Error GET /pr/smm-channels:', err);
    res.status(500).json({ error: 'Failed to list SMM channels', details: err.message });
  }
});

// POST /api/pr/smm-channels — создать канал
app.post(`${API_PREFIX}/pr/smm-channels`, async (req, res) => {
  try {
    const { name, type, url, sortOrder = 0 } = req.body || {};
    if (!name || !type) return res.status(400).json({ error: 'Missing required fields: name, type' });
    if (!['tg', 'vk', 'wa', 'other'].includes(type)) return res.status(400).json({ error: 'type must be tg, vk, wa or other' });
    const rows = await query(
      `INSERT INTO pr_smm_channels (name, type, url, sort_order)
       VALUES ($1, $2, $3, $4)
       RETURNING id, name, type, url, sort_order AS "sortOrder", created_at AS "createdAt"`,
      [String(name).trim(), type, url ? String(url).trim() : null, parseInt(sortOrder, 10) || 0]
    );
    res.status(201).json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(503).json({ error: 'SMM channels table not ready' });
    console.error('Error POST /pr/smm-channels:', err);
    res.status(500).json({ error: 'Failed to create SMM channel', details: err.message });
  }
});

// PUT /api/pr/smm-channels/:id — обновить канал
app.put(`${API_PREFIX}/pr/smm-channels/:id`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const { name, type, url, sortOrder } = req.body || {};
    const existing = await query('SELECT id FROM pr_smm_channels WHERE id = $1', [id]);
    if (!existing || existing.length === 0) return res.status(404).json({ error: 'Channel not found' });
    const updates = [];
    const params = [];
    let idx = 1;
    if (name !== undefined) { updates.push(`name = $${idx}`); params.push(String(name).trim()); idx++; }
    if (type !== undefined) {
      if (!['tg', 'vk', 'wa', 'other'].includes(type)) return res.status(400).json({ error: 'type must be tg, vk, wa or other' });
      updates.push(`type = $${idx}`); params.push(type); idx++;
    }
    if (url !== undefined) { updates.push(`url = $${idx}`); params.push(url ? String(url).trim() : null); idx++; }
    if (sortOrder !== undefined) { updates.push(`sort_order = $${idx}`); params.push(parseInt(sortOrder, 10) || 0); idx++; }
    if (updates.length === 0) {
      const rows = await query(`SELECT id, name, type, url, sort_order AS "sortOrder", created_at AS "createdAt" FROM pr_smm_channels WHERE id = $1`, [id]);
      return res.json(rows[0]);
    }
    params.push(id);
    await query(`UPDATE pr_smm_channels SET ${updates.join(', ')} WHERE id = $${idx}`, params);
    const rows = await query(`SELECT id, name, type, url, sort_order AS "sortOrder", created_at AS "createdAt" FROM pr_smm_channels WHERE id = $1`, [id]);
    res.json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Channel not found' });
    console.error('Error PUT /pr/smm-channels/:id:', err);
    res.status(500).json({ error: 'Failed to update SMM channel', details: err.message });
  }
});

// DELETE /api/pr/smm-channels/:id — удалить канал (и снимки)
app.delete(`${API_PREFIX}/pr/smm-channels/:id`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const result = await query('DELETE FROM pr_smm_channels WHERE id = $1 RETURNING id', [id]);
    if (result.length === 0) return res.status(404).json({ error: 'Channel not found' });
    res.json({ success: true });
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Channel not found' });
    console.error('Error DELETE /pr/smm-channels/:id:', err);
    res.status(500).json({ error: 'Failed to delete SMM channel', details: err.message });
  }
});

// POST /api/pr/smm-channels/:id/snapshot — зафиксировать количество подписчиков
app.post(`${API_PREFIX}/pr/smm-channels/:id/snapshot`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const { subscribersCount, note, recordedAt } = req.body || {};
    const count = subscribersCount != null ? parseInt(subscribersCount, 10) : null;
    if (count == null || count < 0) return res.status(400).json({ error: 'subscribersCount is required and must be >= 0' });
    const existing = await query('SELECT id FROM pr_smm_channels WHERE id = $1', [id]);
    if (!existing || existing.length === 0) return res.status(404).json({ error: 'Channel not found' });
    const recDate = recordedAt ? String(recordedAt).slice(0, 10) : new Date().toISOString().slice(0, 10);
    const rows = await query(
      `INSERT INTO pr_smm_subscriber_snapshots (channel_id, subscribers_count, recorded_at, note)
       VALUES ($1, $2, $3::date, $4)
       RETURNING id, channel_id AS "channelId", subscribers_count AS "subscribersCount", recorded_at AS "recordedAt", note, created_at AS "createdAt"`,
      [id, count, recDate, note ? String(note).trim() : null]
    );
    res.status(201).json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Channel not found' });
    console.error('Error POST /pr/smm-channels/:id/snapshot:', err);
    res.status(500).json({ error: 'Failed to create snapshot', details: err.message });
  }
});

// ========= PR ПРИВЛЕЧЕНИЕ (pr_attraction_actions) =========

// GET /api/pr/attraction-actions — список с фильтром
app.get(`${API_PREFIX}/pr/attraction-actions`, async (req, res) => {
  try {
    const { channelId, actionType, from, to, limit = 50 } = req.query;
    let queryText = `
      SELECT a.id, a.title, a.description, a.channel_id AS "channelId", a.action_type AS "actionType",
             a.action_date AS "actionDate", a.new_subscribers_attributed AS "newSubscribersAttributed",
             a.event_id AS "eventId", a.created_at AS "createdAt", a.created_by AS "createdBy",
             c.name AS "channelName", c.type AS "channelType"
      FROM pr_attraction_actions a
      LEFT JOIN pr_smm_channels c ON c.id = a.channel_id
      WHERE 1=1
    `;
    const params = [];
    let idx = 1;
    if (channelId) { queryText += ` AND a.channel_id = $${idx}`; params.push(parseInt(channelId, 10)); idx++; }
    if (actionType && ['mailing', 'event', 'post', 'other'].includes(String(actionType))) { queryText += ` AND a.action_type = $${idx}`; params.push(actionType); idx++; }
    if (from) { queryText += ` AND a.action_date >= $${idx}::date`; params.push(String(from).slice(0, 10)); idx++; }
    if (to) { queryText += ` AND a.action_date <= $${idx}::date`; params.push(String(to).slice(0, 10)); idx++; }
    queryText += ` ORDER BY a.action_date DESC, a.created_at DESC LIMIT $${idx}`;
    params.push(parseInt(limit, 10) || 50);
    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    if (err.code === '42P01') return res.json([]);
    console.error('Error GET /pr/attraction-actions:', err);
    res.status(500).json({ error: 'Failed to list attraction actions', details: err.message });
  }
});

// POST /api/pr/attraction-actions — создать
app.post(`${API_PREFIX}/pr/attraction-actions`, async (req, res) => {
  try {
    const { title, description, channelId, actionType, actionDate, newSubscribersAttributed, eventId, createdBy } = req.body || {};
    if (!title || !actionType) return res.status(400).json({ error: 'Missing required fields: title, actionType' });
    if (!['mailing', 'event', 'post', 'other'].includes(actionType)) return res.status(400).json({ error: 'actionType must be mailing, event, post or other' });
    const actionDateVal = actionDate ? String(actionDate).slice(0, 10) : new Date().toISOString().slice(0, 10);
    const rows = await query(
      `INSERT INTO pr_attraction_actions (title, description, channel_id, action_type, action_date, new_subscribers_attributed, event_id, created_by)
       VALUES ($1, $2, $3, $4, $5::date, $6, $7, $8)
       RETURNING id, title, description, channel_id AS "channelId", action_type AS "actionType", action_date AS "actionDate",
                 new_subscribers_attributed AS "newSubscribersAttributed", event_id AS "eventId", created_at AS "createdAt", created_by AS "createdBy"`,
      [
        String(title).trim(),
        description ? String(description).trim() : null,
        channelId != null ? parseInt(channelId, 10) : null,
        actionType,
        actionDateVal,
        newSubscribersAttributed != null ? parseInt(newSubscribersAttributed, 10) : null,
        eventId != null ? parseInt(eventId, 10) : null,
        createdBy ? String(createdBy) : null
      ]
    );
    res.status(201).json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(503).json({ error: 'pr_attraction_actions table not ready' });
    console.error('Error POST /pr/attraction-actions:', err);
    res.status(500).json({ error: 'Failed to create attraction action', details: err.message });
  }
});

// PUT /api/pr/attraction-actions/:id — обновить
app.put(`${API_PREFIX}/pr/attraction-actions/:id`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const { title, description, channelId, actionType, actionDate, newSubscribersAttributed, eventId } = req.body || {};
    const existing = await query('SELECT id FROM pr_attraction_actions WHERE id = $1', [id]);
    if (!existing || existing.length === 0) return res.status(404).json({ error: 'Attraction action not found' });
    const updates = [];
    const params = [];
    let idx = 1;
    if (title !== undefined) { updates.push(`title = $${idx}`); params.push(String(title).trim()); idx++; }
    if (description !== undefined) { updates.push(`description = $${idx}`); params.push(description ? String(description).trim() : null); idx++; }
    if (channelId !== undefined) { updates.push(`channel_id = $${idx}`); params.push(channelId != null ? parseInt(channelId, 10) : null); idx++; }
    if (actionType !== undefined) {
      if (!['mailing', 'event', 'post', 'other'].includes(actionType)) return res.status(400).json({ error: 'actionType must be mailing, event, post or other' });
      updates.push(`action_type = $${idx}`); params.push(actionType); idx++;
    }
    if (actionDate !== undefined) { updates.push(`action_date = $${idx}::date`); params.push(String(actionDate).slice(0, 10)); idx++; }
    if (newSubscribersAttributed !== undefined) { updates.push(`new_subscribers_attributed = $${idx}`); params.push(newSubscribersAttributed != null ? parseInt(newSubscribersAttributed, 10) : null); idx++; }
    if (eventId !== undefined) { updates.push(`event_id = $${idx}`); params.push(eventId != null ? parseInt(eventId, 10) : null); idx++; }
    if (updates.length === 0) {
      const rows = await query(
        `SELECT a.id, a.title, a.description, a.channel_id AS "channelId", a.action_type AS "actionType", a.action_date AS "actionDate",
                a.new_subscribers_attributed AS "newSubscribersAttributed", a.event_id AS "eventId", a.created_at AS "createdAt", a.created_by AS "createdBy",
                c.name AS "channelName", c.type AS "channelType"
         FROM pr_attraction_actions a LEFT JOIN pr_smm_channels c ON c.id = a.channel_id WHERE a.id = $1`,
        [id]
      );
      return res.json(rows[0]);
    }
    params.push(id);
    await query(`UPDATE pr_attraction_actions SET ${updates.join(', ')} WHERE id = $${idx}`, params);
    const rows = await query(
      `SELECT a.id, a.title, a.description, a.channel_id AS "channelId", a.action_type AS "actionType", a.action_date AS "actionDate",
              a.new_subscribers_attributed AS "newSubscribersAttributed", a.event_id AS "eventId", a.created_at AS "createdAt", a.created_by AS "createdBy",
              c.name AS "channelName", c.type AS "channelType"
       FROM pr_attraction_actions a LEFT JOIN pr_smm_channels c ON c.id = a.channel_id WHERE a.id = $1`,
      [id]
    );
    res.json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Attraction action not found' });
    console.error('Error PUT /pr/attraction-actions/:id:', err);
    res.status(500).json({ error: 'Failed to update attraction action', details: err.message });
  }
});

// DELETE /api/pr/attraction-actions/:id
app.delete(`${API_PREFIX}/pr/attraction-actions/:id`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const result = await query('DELETE FROM pr_attraction_actions WHERE id = $1 RETURNING id', [id]);
    if (result.length === 0) return res.status(404).json({ error: 'Attraction action not found' });
    res.json({ success: true });
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Attraction action not found' });
    console.error('Error DELETE /pr/attraction-actions/:id:', err);
    res.status(500).json({ error: 'Failed to delete attraction action', details: err.message });
  }
});

// ========= PR ТЕМЫ ПОСТОВ (pr_post_topics) =========

// GET /api/pr/post-topics — список тем графика публикации (фильтр по месяцу, статусу)
app.get(`${API_PREFIX}/pr/post-topics`, async (req, res) => {
  try {
    const { month, status, limit = 50 } = req.query;
    let queryText = `
      SELECT id, title, description, scheduled_date AS "scheduledDate", month, status, created_at AS "createdAt", created_by AS "createdBy",
             approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason"
      FROM pr_post_topics
      WHERE 1=1
    `;
    const params = [];
    if (month) { queryText += ` AND month = $${params.length + 1}`; params.push(String(month).slice(0, 7)); }
    if (status && ['draft', 'pending_approval', 'approved', 'rejected'].includes(String(status))) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }
    queryText += ` ORDER BY scheduled_date ASC, created_at DESC LIMIT $${params.length + 1}`;
    params.push(parseInt(limit, 10) || 50);
    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    if (err.code === '42P01') return res.json([]);
    console.error('Error GET /pr/post-topics:', err);
    res.status(500).json({ error: 'Failed to list post topics', details: err.message });
  }
});

// POST /api/pr/post-topics — создать тему графика публикации
app.post(`${API_PREFIX}/pr/post-topics`, async (req, res) => {
  try {
    const { title, description, scheduledDate, month, status = 'draft', createdBy } = req.body || {};
    if (!title || !scheduledDate || !month) return res.status(400).json({ error: 'Missing required fields: title, scheduledDate, month' });
    const monthVal = String(month).slice(0, 7);
    const scheduledDateVal = String(scheduledDate).slice(0, 10);
    const rows = await query(
      `INSERT INTO pr_post_topics (title, description, scheduled_date, month, status, created_by)
       VALUES ($1, $2, $3::date, $4, $5, $6)
       RETURNING id, title, description, scheduled_date AS "scheduledDate", month, status, created_at AS "createdAt", created_by AS "createdBy",
                 approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason"`,
      [String(title).trim(), description ? String(description).trim() : null, scheduledDateVal, monthVal, status, createdBy ? String(createdBy) : null]
    );
    res.status(201).json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(503).json({ error: 'pr_post_topics table not ready' });
    console.error('Error POST /pr/post-topics:', err);
    res.status(500).json({ error: 'Failed to create post topic', details: err.message });
  }
});

// PUT /api/pr/post-topics/:id — обновить тему графика публикации
app.put(`${API_PREFIX}/pr/post-topics/:id`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const { title, description, scheduledDate, status } = req.body || {};
    const existing = await query('SELECT id, status FROM pr_post_topics WHERE id = $1', [id]);
    if (!existing || existing.length === 0) return res.status(404).json({ error: 'Topic not found' });
    const updates = [];
    const params = [];
    let idx = 1;
    if (title !== undefined) { updates.push(`title = $${idx}`); params.push(String(title).trim()); idx++; }
    if (description !== undefined) { updates.push(`description = $${idx}`); params.push(description ? String(description).trim() : null); idx++; }
    if (scheduledDate !== undefined) { updates.push(`scheduled_date = $${idx}::date`); params.push(String(scheduledDate).slice(0, 10)); idx++; }
    if (status !== undefined && ['draft', 'pending_approval', 'approved', 'rejected'].includes(status)) {
      updates.push(`status = $${idx}`);
      params.push(status);
      if (status === 'approved') {
        updates.push(`approved_at = NOW()`);
      }
      idx++;
    }
    if (updates.length === 0) {
      const rows = await query(`SELECT id, title, description, scheduled_date AS "scheduledDate", month, status, created_at AS "createdAt", created_by AS "createdBy", approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason" FROM pr_post_topics WHERE id = $1`, [id]);
      return res.json(rows[0]);
    }
    params.push(id);
    await query(`UPDATE pr_post_topics SET ${updates.join(', ')} WHERE id = $${idx}`, params);
    const rows = await query(`SELECT id, title, description, scheduled_date AS "scheduledDate", month, status, created_at AS "createdAt", created_by AS "createdBy", approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason" FROM pr_post_topics WHERE id = $1`, [id]);
    res.json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Topic not found' });
    console.error('Error PUT /pr/post-topics/:id:', err);
    res.status(500).json({ error: 'Failed to update post topic', details: err.message });
  }
});

// POST /api/pr/post-topics/:id/approve — одобрить тему графика публикации
app.post(`${API_PREFIX}/pr/post-topics/:id/approve`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const { approvedBy } = req.body || {};
    const rows = await query(
      `UPDATE pr_post_topics SET status = 'approved', approved_at = NOW(), approved_by = $1 WHERE id = $2
       RETURNING id, title, description, scheduled_date AS "scheduledDate", month, status, created_at AS "createdAt", created_by AS "createdBy", approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason"`,
      [approvedBy ? String(approvedBy) : null, id]
    );
    if (rows.length === 0) return res.status(404).json({ error: 'Topic not found' });
    res.json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Topic not found' });
    console.error('Error POST /pr/post-topics/:id/approve:', err);
    res.status(500).json({ error: 'Failed to approve topic', details: err.message });
  }
});

// POST /api/pr/post-topics/:id/reject — отклонить тему графика публикации
app.post(`${API_PREFIX}/pr/post-topics/:id/reject`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const { rejectionReason, approvedBy } = req.body || {};
    const rows = await query(
      `UPDATE pr_post_topics SET status = 'rejected', approved_by = $1, rejection_reason = $2 WHERE id = $3
       RETURNING id, title, description, scheduled_date AS "scheduledDate", month, status, created_at AS "createdAt", created_by AS "createdBy", approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason"`,
      [approvedBy ? String(approvedBy) : null, rejectionReason ? String(rejectionReason).trim() : null, id]
    );
    if (rows.length === 0) return res.status(404).json({ error: 'Topic not found' });
    res.json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Topic not found' });
    console.error('Error POST /pr/post-topics/:id/reject:', err);
    res.status(500).json({ error: 'Failed to reject topic', details: err.message });
  }
});

// DELETE /api/pr/post-topics/:id
app.delete(`${API_PREFIX}/pr/post-topics/:id`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const result = await query('DELETE FROM pr_post_topics WHERE id = $1 RETURNING id', [id]);
    if (result.length === 0) return res.status(404).json({ error: 'Topic not found' });
    res.json({ success: true });
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Topic not found' });
    console.error('Error DELETE /pr/post-topics/:id:', err);
    res.status(500).json({ error: 'Failed to delete topic', details: err.message });
  }
});

// ========= PR ОТЛОЖЕННЫЕ ПОСТЫ (pr_scheduled_posts) =========

// GET /api/pr/scheduled-posts — список отложенных постов
app.get(`${API_PREFIX}/pr/scheduled-posts`, async (req, res) => {
  try {
    const { status, topicId, from, to, limit = 50 } = req.query;
    let queryText = `
      SELECT id, title, content, channel_ids AS "channelIds", scheduled_at AS "scheduledAt", status,
             topic_id AS "topicId", image_url AS "imageUrl", created_at AS "createdAt", created_by AS "createdBy",
             approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason",
             edited_content AS "editedContent", published_at AS "publishedAt"
      FROM pr_scheduled_posts
      WHERE 1=1
    `;
    const params = [];
    if (status && ['draft', 'pending_approval', 'approved', 'rejected', 'edited', 'published'].includes(String(status))) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }
    if (topicId) { queryText += ` AND topic_id = $${params.length + 1}`; params.push(parseInt(topicId, 10)); }
    if (from) { queryText += ` AND scheduled_at >= $${params.length + 1}::timestamptz`; params.push(String(from)); }
    if (to) { queryText += ` AND scheduled_at <= $${params.length + 1}::timestamptz`; params.push(String(to)); }
    queryText += ` ORDER BY scheduled_at DESC LIMIT $${params.length + 1}`;
    params.push(parseInt(limit, 10) || 50);
    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    if (err.code === '42P01') return res.json([]);
    console.error('Error GET /pr/scheduled-posts:', err);
    res.status(500).json({ error: 'Failed to list scheduled posts', details: err.message });
  }
});

// POST /api/pr/scheduled-posts — создать отложенный пост (с возможной загрузкой изображения)
app.post(`${API_PREFIX}/pr/scheduled-posts`, uploadPostImage.single('image'), async (req, res) => {
  try {
    const { title, content, channelIds = [], scheduledAt, status = 'draft', topicId, createdBy } = req.body || {};
    if (!title || !content || !scheduledAt) {
      if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
      return res.status(400).json({ error: 'Missing required fields: title, content, scheduledAt' });
    }
    const channelIdsJson = Array.isArray(channelIds) ? JSON.stringify(channelIds) : '[]';
    const imageUrl = req.file ? `/api/pr/scheduled-posts/image/${req.file.filename}` : null;
    const rows = await query(
      `INSERT INTO pr_scheduled_posts (title, content, channel_ids, scheduled_at, status, topic_id, image_url, created_by)
       VALUES ($1, $2, $3::jsonb, $4::timestamptz, $5, $6, $7, $8)
       RETURNING id, title, content, channel_ids AS "channelIds", scheduled_at AS "scheduledAt", status,
                 topic_id AS "topicId", image_url AS "imageUrl", created_at AS "createdAt", created_by AS "createdBy",
                 approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason",
                 edited_content AS "editedContent", published_at AS "publishedAt"`,
      [
        String(title).trim(),
        String(content).trim(),
        channelIdsJson,
        String(scheduledAt),
        status,
        topicId != null ? parseInt(topicId, 10) : null,
        imageUrl,
        createdBy ? String(createdBy) : null
      ]
    );
    res.status(201).json(rows[0]);
  } catch (err) {
    if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
    if (err.code === '42P01') return res.status(503).json({ error: 'pr_scheduled_posts table not ready' });
    console.error('Error POST /pr/scheduled-posts:', err);
    res.status(500).json({ error: 'Failed to create scheduled post', details: err.message });
  }
});

// PUT /api/pr/scheduled-posts/:id — обновить пост (с возможной загрузкой изображения)
app.put(`${API_PREFIX}/pr/scheduled-posts/:id`, uploadPostImage.single('image'), async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) {
      if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
      return res.status(400).json({ error: 'Invalid id' });
    }
    const { title, content, channelIds, scheduledAt, status, topicId, removeImage } = req.body || {};
    const existing = await query('SELECT id, image_url FROM pr_scheduled_posts WHERE id = $1', [id]);
    if (!existing || existing.length === 0) {
      if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
      return res.status(404).json({ error: 'Post not found' });
    }
    const updates = [];
    const params = [];
    let idx = 1;
    if (title !== undefined) { updates.push(`title = $${idx}`); params.push(String(title).trim()); idx++; }
    if (content !== undefined) { updates.push(`content = $${idx}`); params.push(String(content).trim()); idx++; }
    if (channelIds !== undefined) {
      const channelIdsJson = Array.isArray(channelIds) ? JSON.stringify(channelIds) : '[]';
      updates.push(`channel_ids = $${idx}::jsonb`);
      params.push(channelIdsJson);
      idx++;
    }
    if (scheduledAt !== undefined) { updates.push(`scheduled_at = $${idx}::timestamptz`); params.push(String(scheduledAt)); idx++; }
    if (req.file) {
      const imageUrl = `/api/pr/scheduled-posts/image/${req.file.filename}`;
      // Удалить старое изображение если есть
      if (existing[0].image_url) {
        const oldFilename = existing[0].image_url.split('/').pop();
        const oldPath = path.join(postImagesDir, oldFilename);
        if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
      }
      updates.push(`image_url = $${idx}`);
      params.push(imageUrl);
      idx++;
    } else if (removeImage === 'true' || removeImage === true) {
      if (existing[0].image_url) {
        const oldFilename = existing[0].image_url.split('/').pop();
        const oldPath = path.join(postImagesDir, oldFilename);
        if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
      }
      updates.push(`image_url = NULL`);
    }
    if (status !== undefined && ['draft', 'pending_approval', 'approved', 'rejected', 'edited', 'published'].includes(status)) {
      updates.push(`status = $${idx}`);
      params.push(status);
      if (status === 'approved') {
        updates.push(`approved_at = NOW()`);
      } else if (status === 'published') {
        updates.push(`published_at = NOW()`);
      }
      idx++;
    }
    if (topicId !== undefined) { updates.push(`topic_id = $${idx}`); params.push(topicId != null ? parseInt(topicId, 10) : null); idx++; }
    if (updates.length === 0) {
      const rows = await query(`SELECT id, title, content, channel_ids AS "channelIds", scheduled_at AS "scheduledAt", status, topic_id AS "topicId", image_url AS "imageUrl", created_at AS "createdAt", created_by AS "createdBy", approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason", edited_content AS "editedContent", published_at AS "publishedAt" FROM pr_scheduled_posts WHERE id = $1`, [id]);
      return res.json(rows[0]);
    }
    params.push(id);
    await query(`UPDATE pr_scheduled_posts SET ${updates.join(', ')} WHERE id = $${idx}`, params);
    const rows = await query(`SELECT id, title, content, channel_ids AS "channelIds", scheduled_at AS "scheduledAt", status, topic_id AS "topicId", image_url AS "imageUrl", created_at AS "createdAt", created_by AS "createdBy", approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason", edited_content AS "editedContent", published_at AS "publishedAt" FROM pr_scheduled_posts WHERE id = $1`, [id]);
    res.json(rows[0]);
  } catch (err) {
    if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
    if (err.code === '42P01') return res.status(404).json({ error: 'Post not found' });
    console.error('Error PUT /pr/scheduled-posts/:id:', err);
    res.status(500).json({ error: 'Failed to update scheduled post', details: err.message });
  }
});

// POST /api/pr/scheduled-posts/:id/approve — одобрить пост
app.post(`${API_PREFIX}/pr/scheduled-posts/:id/approve`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const { approvedBy } = req.body || {};
    const rows = await query(
      `UPDATE pr_scheduled_posts SET status = 'approved', approved_at = NOW(), approved_by = $1 WHERE id = $2
       RETURNING id, title, content, channel_ids AS "channelIds", scheduled_at AS "scheduledAt", status, topic_id AS "topicId", image_url AS "imageUrl", created_at AS "createdAt", created_by AS "createdBy", approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason", edited_content AS "editedContent", published_at AS "publishedAt"`,
      [approvedBy ? String(approvedBy) : null, id]
    );
    if (rows.length === 0) return res.status(404).json({ error: 'Post not found' });
    res.json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Post not found' });
    console.error('Error POST /pr/scheduled-posts/:id/approve:', err);
    res.status(500).json({ error: 'Failed to approve post', details: err.message });
  }
});

// POST /api/pr/scheduled-posts/:id/reject — отклонить пост (с причиной)
app.post(`${API_PREFIX}/pr/scheduled-posts/:id/reject`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const { rejectionReason, approvedBy } = req.body || {};
    const rows = await query(
      `UPDATE pr_scheduled_posts SET status = 'rejected', approved_by = $1, rejection_reason = $2 WHERE id = $3
       RETURNING id, title, content, channel_ids AS "channelIds", scheduled_at AS "scheduledAt", status, topic_id AS "topicId", image_url AS "imageUrl", created_at AS "createdAt", created_by AS "createdBy", approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason", edited_content AS "editedContent", published_at AS "publishedAt"`,
      [approvedBy ? String(approvedBy) : null, rejectionReason ? String(rejectionReason).trim() : null, id]
    );
    if (rows.length === 0) return res.status(404).json({ error: 'Post not found' });
    res.json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Post not found' });
    console.error('Error POST /pr/scheduled-posts/:id/reject:', err);
    res.status(500).json({ error: 'Failed to reject post', details: err.message });
  }
});

// POST /api/pr/scheduled-posts/:id/send-to-edit — отправить на редактирование
app.post(`${API_PREFIX}/pr/scheduled-posts/:id/send-to-edit`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const { editedContent, approvedBy } = req.body || {};
    if (!editedContent) return res.status(400).json({ error: 'editedContent is required' });
    const rows = await query(
      `UPDATE pr_scheduled_posts SET status = 'edited', edited_content = $1, approved_by = $2 WHERE id = $3
       RETURNING id, title, content, channel_ids AS "channelIds", scheduled_at AS "scheduledAt", status, topic_id AS "topicId", image_url AS "imageUrl", created_at AS "createdAt", created_by AS "createdBy", approved_at AS "approvedAt", approved_by AS "approvedBy", rejection_reason AS "rejectionReason", edited_content AS "editedContent", published_at AS "publishedAt"`,
      [String(editedContent).trim(), approvedBy ? String(approvedBy) : null, id]
    );
    if (rows.length === 0) return res.status(404).json({ error: 'Post not found' });
    res.json(rows[0]);
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Post not found' });
    console.error('Error POST /pr/scheduled-posts/:id/send-to-edit:', err);
    res.status(500).json({ error: 'Failed to send post to edit', details: err.message });
  }
});

// DELETE /api/pr/scheduled-posts/:id
app.delete(`${API_PREFIX}/pr/scheduled-posts/:id`, async (req, res) => {
  try {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) return res.status(400).json({ error: 'Invalid id' });
    const result = await query('DELETE FROM pr_scheduled_posts WHERE id = $1 RETURNING id', [id]);
    if (result.length === 0) return res.status(404).json({ error: 'Post not found' });
    res.json({ success: true });
  } catch (err) {
    if (err.code === '42P01') return res.status(404).json({ error: 'Post not found' });
    console.error('Error DELETE /pr/scheduled-posts/:id:', err);
    res.status(500).json({ error: 'Failed to delete scheduled post', details: err.message });
  }
});

// ========= НАСТРОЙКИ ПАРСИНГА (API ОТЗЫВОВ) =========

// GET /api/pr/parsing-settings - получить настройки
app.get(`${API_PREFIX}/pr/parsing-settings`, async (req, res) => {
  try {
    const rows = await query(`
      SELECT 
        id,
        source,
        enabled,
        url_template AS "urlTemplate",
        api_key AS "apiKey",
        parsing_interval_hours AS "parsingIntervalHours",
        last_parsed_at AS "lastParsedAt",
        settings,
        created_at AS "createdAt",
        updated_at AS "updatedAt"
      FROM parsing_settings 
      ORDER BY source
    `);

    const processedRows = rows.map(row => {
      let settings = row.settings;
      let buildingId = null;
      if (settings && typeof settings === 'object') {
        buildingId = settings.building_id || null;
      } else if (settings && typeof settings === 'string') {
        try {
          const parsed = JSON.parse(settings);
          buildingId = parsed.building_id || null;
          settings = parsed;
        } catch (err) {
          console.warn('[server] Не удалось распарсить settings для', row.source, err);
        }
      }
      return {
        ...row,
        settings: settings || {},
        buildingId: buildingId
      };
    });

    res.json(processedRows);
  } catch (err) {
    console.error('Error fetching parsing settings:', err);
    res.status(500).json({ error: 'Failed to fetch parsing settings' });
  }
});

// PUT /api/pr/parsing-settings/:source - обновить настройки
app.put(`${API_PREFIX}/pr/parsing-settings/:source`, async (req, res) => {
  try {
    const { source } = req.params;
    const { enabled, url_template, api_key, parsing_interval_hours, settings } = req.body;

    if (!['yandex_maps', '2gis'].includes(source)) {
      return res.status(400).json({ error: 'Invalid source. Must be yandex_maps or 2gis' });
    }

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (enabled !== undefined) {
      updates.push(`enabled = $${paramIndex++}`);
      params.push(enabled);
    }
    if (url_template !== undefined) {
      updates.push(`url_template = $${paramIndex++}`);
      params.push(url_template);
    }
    if (api_key !== undefined) {
      updates.push(`api_key = $${paramIndex++}`);
      params.push(api_key);
    }
    if (parsing_interval_hours !== undefined) {
      updates.push(`parsing_interval_hours = $${paramIndex++}`);
      params.push(parsing_interval_hours);
    }
    if (settings !== undefined) {
      updates.push(`settings = $${paramIndex++}`);
      params.push(JSON.stringify(settings));
    }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    updates.push(`updated_at = NOW()`);
    params.push(source);

    const existing = await query('SELECT id FROM parsing_settings WHERE source = $1', [source]);

    if (existing.length === 0) {
      updates.unshift('source', 'enabled', 'url_template', 'api_key', 'parsing_interval_hours', 'settings');
      params.unshift(source, enabled !== undefined ? enabled : true, url_template || '', api_key || null, parsing_interval_hours || 24, settings ? JSON.stringify(settings) : '{}');
      await query(
        `INSERT INTO parsing_settings (${updates.slice(0, -1).join(', ')}, created_at, updated_at)
         VALUES (${updates.slice(0, -1).map((_, i) => `$${i + 1}`).join(', ')}, NOW(), NOW())`,
        params.slice(0, -1)
      );
    } else {
      await query(
        `UPDATE parsing_settings SET ${updates.join(', ')} WHERE source = $${paramIndex}`,
        params
      );
    }

    const result = await query(`
      SELECT id, source, enabled, url_template AS "urlTemplate", api_key AS "apiKey",
             parsing_interval_hours AS "parsingIntervalHours", last_parsed_at AS "lastParsedAt",
             settings, created_at AS "createdAt", updated_at AS "updatedAt"
      FROM parsing_settings WHERE source = $1`,
      [source]
    );
    res.json(result[0] || {});
  } catch (err) {
    console.error('Error updating parsing settings:', err);
    res.status(500).json({ error: 'Failed to update parsing settings' });
  }
});

// POST /api/pr/parsing-settings/:source/test - тест API (без сохранения в БД)
app.post(`${API_PREFIX}/pr/parsing-settings/:source/test`, async (req, res) => {
  try {
    const { source } = req.params;

    if (!['yandex_maps', '2gis'].includes(source)) {
      return res.status(400).json({ error: 'Invalid source' });
    }

    const settingsResult = await query('SELECT * FROM parsing_settings WHERE source = $1', [source]);
    if (settingsResult.length === 0) {
      return res.status(404).json({ error: 'Настройки парсинга не найдены' });
    }

    const settings = settingsResult[0];
    const apiKey = (settings.api_key || '').trim();
    if (!apiKey) {
      return res.status(400).json({
        error: 'API key required',
        details: 'Укажите API ключ в настройках'
      });
    }

    const reviewApiService = require('./reviewApiService');
    const fetched = await reviewApiService.fetchReviewsFromApi(source, settings);
    const found = fetched.length;

    res.json({
      success: true,
      parsed: found,
      found,
      message: `Тестовый запрос завершен. Найдено отзывов: ${found}`
    });
  } catch (err) {
    console.error('Error testing parsing API:', err);
    res.status(500).json({ error: 'Failed to test API', details: err.message });
  }
});

// POST /api/pr/reviews/fetch - загрузка отзывов через API
app.post(`${API_PREFIX}/pr/reviews/fetch`, async (req, res) => {
  try {
    const { source } = req.body;

    if (!source) {
      return res.status(400).json({
        error: 'Source is required',
        details: 'Укажите источник: 2gis или yandex_maps'
      });
    }
    if (!['yandex_maps', '2gis'].includes(source)) {
      return res.status(400).json({ error: 'Invalid source. Must be yandex_maps or 2gis' });
    }

    const settingsResult = await query('SELECT * FROM parsing_settings WHERE source = $1', [source]);
    if (settingsResult.length === 0) {
      return res.status(400).json({
        error: 'Настройки парсинга не найдены',
        details: 'Укажите API ключ в Настройках → Интеграции'
      });
    }

    const settings = settingsResult[0];
    const apiKey = (settings.api_key || '').trim();
    if (!apiKey) {
      return res.status(400).json({
        error: 'API key required',
        details: 'Укажите API ключ в Настройках → Интеграции'
      });
    }

    const reviewApiService = require('./reviewApiService');
    const fetched = await reviewApiService.fetchReviewsFromApi(source, settings);
    const found = fetched.length;

    const errors = [];
    let parsed = 0;
    const client = await pool.connect();

    try {
      for (const review of fetched) {
        const text = (review.text || '').trim();
        const rating = Math.max(1, Math.min(10, parseInt(review.rating, 10) || 5));
        const date = (review.date || new Date().toISOString().split('T')[0]).substring(0, 10);
        if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
        if (text.length < 10) continue;

        const buildingId = review.building_id || null;
        const sourceUrl = review.source_url || null;
        const authorName = review.author_name || 'Аноним';

        const existing = await client.query(
          `SELECT id FROM reviews WHERE source = $1 AND (source_url = $2 OR (text = $3 AND date = $4)) LIMIT 1`,
          [source, sourceUrl, text, date]
        );

        if (existing.rows.length > 0) continue;

        await client.query(
          `INSERT INTO reviews (building_id, source, source_url, author_name, text, rating, date, status)
           VALUES ($1, $2, $3, $4, $5, $6, $7, 'new')`,
          [buildingId, source, sourceUrl, authorName, text.substring(0, 10000), rating, date]
        );
        parsed++;
      }

      await client.query(
        'UPDATE parsing_settings SET last_parsed_at = NOW() WHERE source = $1',
        [source]
      );
    } finally {
      client.release();
    }

    const message = `Загрузка завершена. Найдено: ${found}, сохранено: ${parsed}`;
    console.log('[server]', message);

    res.json({
      success: true,
      parsed,
      found,
      errors: errors.length ? errors : undefined,
      message
    });
  } catch (err) {
    console.error('[server] Error fetching reviews via API:', err);
    res.status(500).json({
      success: false,
      error: 'Failed to fetch reviews',
      details: err.message
    });
  }
});

// ========= NPS ОПРОСЫ =========

// GET /api/pr/nps-surveys - список опросов
app.get(`${API_PREFIX}/pr/nps-surveys`, async (req, res) => {
  try {
    const { building_id, status } = req.query;
    let queryText = `
      SELECT s.*, 
             (b.data->'passport')->>'address' as address,
             COUNT(r.id) as responses_count,
             AVG(r.score)::numeric(3,1) as avg_score
      FROM nps_surveys s
      LEFT JOIN buildings b ON s.building_id = b.id
      LEFT JOIN nps_responses r ON s.id = r.survey_id
      WHERE 1=1
    `;
    const params = [];

    if (building_id) {
      queryText += ` AND s.building_id = $${params.length + 1}`;
      params.push(building_id);
    }
    if (status) {
      queryText += ` AND s.status = $${params.length + 1}`;
      params.push(status);
    }

    queryText += ' GROUP BY s.id, b.data ORDER BY s.created_at DESC';

    const rows = await query(queryText, params);
    
    // Преобразуем snake_case в camelCase
    const formattedRows = rows.map(row => ({
      id: row.id,
      buildingId: row.building_id,
      title: row.title,
      description: row.description,
      status: row.status,
      accessKey: row.access_key,
      publishedAt: row.published_at,
      expiresAt: row.expires_at,
      createdBy: row.created_by,
      createdAt: row.created_at,
      updatedAt: row.updated_at,
      address: row.address,
      responsesCount: parseInt(row.responses_count) || 0,
      avgScore: parseFloat(row.avg_score) || 0
    }));
    
    res.json(formattedRows);
  } catch (err) {
    console.error('Error fetching NPS surveys:', err);
    res.status(500).json({ error: 'Failed to fetch NPS surveys' });
  }
});

// GET /api/pr/nps-surveys/:id - детали опроса
app.get(`${API_PREFIX}/pr/nps-surveys/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    
    // Сначала получаем опрос и адрес
    const surveyResult = await query(
      `SELECT s.*, 
              (b.data->'passport')->>'address' as address
       FROM nps_surveys s
       LEFT JOIN buildings b ON s.building_id = b.id
       WHERE s.id = $1`,
      [id]
    );

    if (surveyResult.length === 0) {
      return res.status(404).json({ error: 'Survey not found' });
    }

    const survey = surveyResult[0];
    
    // Затем получаем статистику по ответам
    const statsResult = await query(
      `SELECT 
              COUNT(*) as responses_count,
              AVG(score)::numeric(3,1) as avg_score
       FROM nps_responses
       WHERE survey_id = $1`,
      [id]
    );

    const stats = statsResult[0] || { responses_count: 0, avg_score: null };

    res.json({
      id: survey.id,
      buildingId: survey.building_id,
      title: survey.title,
      description: survey.description,
      status: survey.status,
      accessKey: survey.access_key,
      publishedAt: survey.published_at,
      expiresAt: survey.expires_at,
      createdBy: survey.created_by,
      createdAt: survey.created_at,
      updatedAt: survey.updated_at,
      address: survey.address,
      responsesCount: parseInt(stats.responses_count) || 0,
      avgScore: parseFloat(stats.avg_score) || 0
    });
  } catch (err) {
    console.error('Error fetching NPS survey:', err);
    res.status(500).json({ error: 'Failed to fetch NPS survey' });
  }
});

// POST /api/pr/nps-surveys - создание опроса
app.post(`${API_PREFIX}/pr/nps-surveys`, async (req, res) => {
  try {
    const { building_id, title, description, expires_at, created_by } = req.body;

    if (!building_id || !created_by) {
      return res.status(400).json({ error: 'building_id and created_by are required' });
    }

    // Генерируем уникальный ключ доступа
    const accessKey = `nps-${building_id}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

    const result = await query(
      `INSERT INTO nps_surveys (building_id, title, description, access_key, expires_at, created_by)
       VALUES ($1, $2, $3, $4, $5, $6)
       RETURNING *`,
      [building_id, title || 'Опрос удовлетворенности', description || null, accessKey, expires_at || null, created_by]
    );

    const row = result[0];
    try {
      await notificationService.createNotificationForResponsibleZone(pool, 'pr', 'nps', {
        type: 'nps_survey',
        title: 'Новый NPS-опрос',
        body: (row.title || 'Опрос удовлетворенности').toString().slice(0, 150),
        entityType: 'nps_survey',
        entityId: String(row.id),
      });
    } catch (notifErr) {
      console.warn('Notification (NPS create):', notifErr.message);
    }
    res.status(201).json({
      id: row.id,
      buildingId: row.building_id,
      title: row.title,
      description: row.description,
      status: row.status,
      accessKey: row.access_key,
      publishedAt: row.published_at,
      expiresAt: row.expires_at,
      createdBy: row.created_by,
      createdAt: row.created_at,
      updatedAt: row.updated_at
    });
  } catch (err) {
    console.error('Error creating NPS survey:', err);
    res.status(500).json({ error: 'Failed to create NPS survey' });
  }
});

// PUT /api/pr/nps-surveys/:id - обновление опроса
app.put(`${API_PREFIX}/pr/nps-surveys/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { title, description, status, expires_at } = req.body;

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (title !== undefined) {
      updates.push(`title = $${paramIndex++}`);
      params.push(title);
    }
    if (description !== undefined) {
      updates.push(`description = $${paramIndex++}`);
      params.push(description);
    }
    if (status !== undefined) {
      updates.push(`status = $${paramIndex++}`);
      params.push(status);
      if (status === 'active' && !req.body.published_at) {
        updates.push(`published_at = NOW()`);
      }
    }
    if (expires_at !== undefined) {
      updates.push(`expires_at = $${paramIndex++}`);
      params.push(expires_at);
    }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    updates.push(`updated_at = NOW()`);
    params.push(id);

    const result = await query(
      `UPDATE nps_surveys SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      params
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Survey not found' });
    }

    const row = result[0];
    try {
      await notificationService.createNotificationForResponsibleZone(pool, 'pr', 'nps', {
        type: 'nps_survey',
        title: 'Изменение NPS-опроса',
        body: (row.title || 'Опрос').toString().slice(0, 150),
        entityType: 'nps_survey',
        entityId: String(row.id),
      });
    } catch (notifErr) {
      console.warn('Notification (NPS update):', notifErr.message);
    }
    res.json({
      id: row.id,
      buildingId: row.building_id,
      title: row.title,
      description: row.description,
      status: row.status,
      accessKey: row.access_key,
      publishedAt: row.published_at,
      expiresAt: row.expires_at,
      createdBy: row.created_by,
      createdAt: row.created_at,
      updatedAt: row.updated_at
    });
  } catch (err) {
    console.error('Error updating NPS survey:', err);
    res.status(500).json({ error: 'Failed to update NPS survey' });
  }
});

// GET /api/pr/nps-surveys/:id/responses - ответы на опрос (с фильтром по месяцу)
app.get(`${API_PREFIX}/pr/nps-surveys/:id/responses`, async (req, res) => {
  try {
    const { id } = req.params;
    const { month, year } = req.query; // Параметры для фильтрации по месяцу
    
    let queryText = `SELECT * FROM nps_responses WHERE survey_id = $1`;
    const params = [id];
    
    // Добавляем фильтр по месяцу и году, если указаны
    if (month && year) {
      queryText += ` AND EXTRACT(MONTH FROM created_at) = $${params.length + 1} AND EXTRACT(YEAR FROM created_at) = $${params.length + 2}`;
      params.push(parseInt(month), parseInt(year));
    }
    
    queryText += ` ORDER BY created_at DESC`;
    
    const rows = await query(queryText, params);

    const formattedRows = rows.map(row => ({
      id: row.id,
      surveyId: row.survey_id,
      buildingId: row.building_id,
      score: row.score,
      comment: row.comment,
      respondentName: row.respondent_name,
      apartment: row.apartment,
      phone: row.phone,
      email: row.email,
      createdAt: row.created_at
    }));

    res.json(formattedRows);
  } catch (err) {
    console.error('Error fetching NPS responses:', err);
    res.status(500).json({ error: 'Failed to fetch NPS responses' });
  }
});

// POST /api/pr/nps-surveys/:id/responses - добавление ответа (публичный endpoint)
app.post(`${API_PREFIX}/pr/nps-surveys/:id/responses`, async (req, res) => {
  try {
    const { id } = req.params;
    const { score, comment, respondent_name, apartment, phone, email, access_key } = req.body;

    if (!score || score < 0 || score > 10) {
      return res.status(400).json({ error: 'Score must be between 0 and 10' });
    }

    // Проверяем ключ доступа
    const surveyResult = await query('SELECT * FROM nps_surveys WHERE id = $1', [id]);
    if (surveyResult.length === 0) {
      return res.status(404).json({ error: 'Survey not found' });
    }

    const survey = surveyResult[0];
    if (survey.access_key !== access_key) {
      return res.status(403).json({ error: 'Invalid access key' });
    }

    if (survey.status !== 'active') {
      return res.status(400).json({ error: 'Survey is not active' });
    }

    const result = await query(
      `INSERT INTO nps_responses (survey_id, building_id, score, comment, respondent_name, apartment, phone, email)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
       RETURNING *`,
      [id, survey.building_id, score, comment || null, respondent_name || null, apartment || null, phone || null, email || null]
    );

    const row = result[0];
    res.status(201).json({
      id: row.id,
      surveyId: row.survey_id,
      buildingId: row.building_id,
      score: row.score,
      comment: row.comment,
      respondentName: row.respondent_name,
      apartment: row.apartment,
      phone: row.phone,
      email: row.email,
      createdAt: row.created_at
    });
  } catch (err) {
    console.error('Error creating NPS response:', err);
    res.status(500).json({ error: 'Failed to create NPS response' });
  }
});

// GET /api/pr/nps-surveys/:id/stats - статистика по опросу (с фильтром по месяцу)
app.get(`${API_PREFIX}/pr/nps-surveys/:id/stats`, async (req, res) => {
  try {
    const { id } = req.params;
    const { month, year } = req.query; // Параметры для фильтрации по месяцу
    
    let queryText = `
      SELECT 
        COUNT(*) as total_responses,
        AVG(score)::numeric(3,1) as avg_score,
        COUNT(*) FILTER (WHERE score >= 9) as promoters,
        COUNT(*) FILTER (WHERE score >= 7 AND score <= 8) as passives,
        COUNT(*) FILTER (WHERE score <= 6) as detractors,
        (COUNT(*) FILTER (WHERE score >= 9)::numeric / NULLIF(COUNT(*), 0) * 100)::numeric(5,1) as promoter_percent,
        (COUNT(*) FILTER (WHERE score <= 6)::numeric / NULLIF(COUNT(*), 0) * 100)::numeric(5,1) as detractor_percent
       FROM nps_responses
       WHERE survey_id = $1
    `;
    const params = [id];
    
    // Добавляем фильтр по месяцу и году, если указаны
    if (month && year) {
      queryText += ` AND EXTRACT(MONTH FROM created_at) = $${params.length + 1} AND EXTRACT(YEAR FROM created_at) = $${params.length + 2}`;
      params.push(parseInt(month), parseInt(year));
    }
    
    const result = await query(queryText, params);

    if (result.length === 0) {
      return res.json({
        totalResponses: 0,
        avgScore: 0,
        promoters: 0,
        passives: 0,
        detractors: 0,
        nps: 0,
        promoterPercent: 0,
        detractorPercent: 0
      });
    }

    const row = result[0];
    const promoters = parseInt(row.promoters) || 0;
    const detractors = parseInt(row.detractors) || 0;
    const total = parseInt(row.total_responses) || 0;
    const nps = total > 0 ? Math.round((promoters / total * 100) - (detractors / total * 100)) : 0;

    res.json({
      totalResponses: total,
      avgScore: parseFloat(row.avg_score) || 0,
      promoters: promoters,
      passives: parseInt(row.passives) || 0,
      detractors: detractors,
      nps: nps,
      promoterPercent: parseFloat(row.promoter_percent) || 0,
      detractorPercent: parseFloat(row.detractor_percent) || 0
    });
  } catch (err) {
    console.error('Error fetching NPS survey stats:', err);
    res.status(500).json({ error: 'Failed to fetch NPS survey stats' });
  }
});

// ========= НАСТРОЙКИ КОМПАНИИ =========

// GET /api/settings/company - получить настройки компании
app.get(`${API_PREFIX}/settings/company`, async (req, res) => {
  try {
    const result = await query('SELECT * FROM company_settings WHERE id = 1 LIMIT 1');
    
    if (result.length === 0) {
      // Возвращаем дефолтные значения
      return res.json({
        id: 1,
        name: 'Управляющая компания "Дружба"',
        fullName: 'ООО "Управляющая компания Дружба"',
        address: 'г. Уфа, ул. Ленина, 1',
        phone: '+7 (347) 123-45-67',
        email: 'info@uk-druzhba.ru',
        website: '',
        licenseNumber: 'Лицензия №12345',
        licenseValidUntil: null,
        logoUrl: ''
      });
    }

    const row = result[0];
    res.json({
      id: row.id,
      name: row.name,
      fullName: row.full_name,
      address: row.address,
      phone: row.phone,
      email: row.email,
      website: row.website,
      licenseNumber: row.license_number,
      licenseValidUntil: row.license_valid_until,
      logoUrl: row.logo_url
    });
  } catch (err) {
    // #region agent log
    fetch('http://127.0.0.1:7242/ingest/22a1555a-4536-4181-8ad4-9c1b25c65316', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        sessionId: 'debug-session',
        runId: 'post-fix',
        hypothesisId: 'H1',
        location: 'backend/server.js:11987',
        message: 'Error fetching company settings',
        data: {
          code: err.code,
          severity: err.severity,
          detail: err.detail
        },
        timestamp: Date.now()
      })
    }).catch(() => {});
    // #endregion agent log
    console.error('Error fetching company settings:', err);
    res.status(500).json({ error: 'Failed to fetch company settings' });
  }
});

// PUT /api/settings/company - обновить настройки компании
app.put(`${API_PREFIX}/settings/company`, async (req, res) => {
  try {
    const { name, fullName, address, phone, email, website, licenseNumber, licenseValidUntil, logoUrl } = req.body;

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (name !== undefined) {
      updates.push(`name = $${paramIndex++}`);
      params.push(name);
    }
    if (fullName !== undefined) {
      updates.push(`full_name = $${paramIndex++}`);
      params.push(fullName);
    }
    if (address !== undefined) {
      updates.push(`address = $${paramIndex++}`);
      params.push(address);
    }
    if (phone !== undefined) {
      updates.push(`phone = $${paramIndex++}`);
      params.push(phone);
    }
    if (email !== undefined) {
      updates.push(`email = $${paramIndex++}`);
      params.push(email);
    }
    if (website !== undefined) {
      updates.push(`website = $${paramIndex++}`);
      params.push(website);
    }
    if (licenseNumber !== undefined) {
      updates.push(`license_number = $${paramIndex++}`);
      params.push(licenseNumber);
    }
    if (licenseValidUntil !== undefined) {
      updates.push(`license_valid_until = $${paramIndex++}`);
      params.push(licenseValidUntil);
    }
    if (logoUrl !== undefined) {
      updates.push(`logo_url = $${paramIndex++}`);
      params.push(logoUrl);
    }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    updates.push(`updated_at = NOW()`);

    // Проверяем, существует ли запись
    const existing = await query('SELECT id FROM company_settings WHERE id = 1');
    
    if (existing.length === 0) {
      // Создаем новую запись
      await query(
        `INSERT INTO company_settings (id, name, full_name, address, phone, email, website, license_number, license_valid_until, logo_url)
         VALUES (1, $1, $2, $3, $4, $5, $6, $7, $8, $9)
         ON CONFLICT (id) DO UPDATE SET ${updates.join(', ')}`,
        [name || 'Управляющая компания', fullName || null, address || null, phone || null, email || null, website || null, licenseNumber || null, licenseValidUntil || null, logoUrl || null]
      );
    } else {
      // Обновляем существующую
      params.push(1);
      await query(
        `UPDATE company_settings SET ${updates.join(', ')} WHERE id = $${paramIndex}`,
        params
      );
    }

    const result = await query('SELECT * FROM company_settings WHERE id = 1');
    const row = result[0];
    res.json({
      id: row.id,
      name: row.name,
      fullName: row.full_name,
      address: row.address,
      phone: row.phone,
      email: row.email,
      website: row.website,
      licenseNumber: row.license_number,
      licenseValidUntil: row.license_valid_until,
      logoUrl: row.logo_url
    });
  } catch (err) {
    console.error('Error updating company settings:', err);
    res.status(500).json({ error: 'Failed to update company settings' });
  }
});

// ========= ДАННЫЕ ДЛЯ ОТЧЕТА ЖИТЕЛЯМ =========

// GET /api/pr/reports/:id/data - получить все данные для отчета
// Маппинг пунктов отчёта → источник:
//   stats.appsQuality, appsTotal, appsCompleted ← applications (COUNT, completed, completed_on_time за период)
//   stats.tasksTotal, tasksCompleted ← календарный план дома (building.data.annualPlan за период месяц/год), иначе fallback applications
//   stats.fundsCollected, fundsSpent, fundsBalance ← building_financial_data (SUM total_income, total_expenses, balance)
//   stats.debtCasesWon, debtCollected ← applications (category = 'debt_collection', metadata->>'debt_collected')
//   expenses.total, byCategory, details ← building_financial_data.expenses_by_items + ключевые слова
//   events ← только pr_events (модуль PR Мероприятия) по дому и периоду
//   nps.* ← nps_building_stats или nps_responses (расчёт и сохранение в nps_building_stats)
//   workPhotos ← work_photos по building_id и work_date в периоде (task_id для привязки к задаче)
//   planItems ← applications (deadline_at в следующем месяце, status != 'completed')
//   building, company ← buildings, company_settings
app.get(`${API_PREFIX}/pr/reports/:id/data`, async (req, res) => {
  try {
    const { id } = req.params;
    const { month, year, building_id, force_refresh } = req.query;

    // Если id это buildingId (строка, не число), работаем напрямую с buildingId
    let targetBuildingId = building_id;
    let report = null;
    
    // Проверяем, является ли id числом (reportId) или строкой (buildingId). UUID и строковые id не считаем числовыми.
    const isNumericId = /^\d+$/.test(String(id).trim());
    
    if (isNumericId) {
      // Получаем отчет по ID
      const reportResult = await query(
        'SELECT * FROM resident_reports WHERE id = $1',
        [id]
      );

      if (reportResult.length === 0) {
        return res.status(404).json({ error: 'Report not found' });
      }

      report = reportResult[0];
      targetBuildingId = building_id || report.building_id;
    } else {
      // id это buildingId (строка, в т.ч. UUID)
      targetBuildingId = id;
    }

    if (!targetBuildingId) {
      return res.status(400).json({ error: 'Не указан дом (building_id) для загрузки данных отчёта' });
    }

    // Определяем период отчета
    let periodStart, periodEnd;
    
    // Если указаны month и year в запросе, используем их
    if (month && year) {
      const targetYear = parseInt(year);
      const targetMonth = parseInt(month) - 1;
      periodStart = new Date(targetYear, targetMonth, 1);
      periodEnd = new Date(targetYear, targetMonth + 1, 0, 23, 59, 59);
    } else if (report && report.period_start && report.period_end) {
      // Используем период из отчета
      periodStart = new Date(report.period_start);
      periodEnd = new Date(report.period_end);
    } else {
      // Если период не указан, используем текущий месяц
      const now = new Date();
      periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
      periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
    }

    const periodStartDate = periodStart.toISOString().slice(0, 10);
    const periodEndDate = periodEnd.toISOString().slice(0, 10);

    // При force_refresh не используем кэш — всегда пересчитываем из таблиц (фото, NPS, заявки и т.д.)
    const useCache = !force_refresh;

    // Сначала пробуем взять сохраненные данные отчета (дом + период), если не запрошено принудительное обновление
    let savedReportDataResult = [];
    if (useCache) {
      savedReportDataResult = await query(
        `SELECT * FROM resident_report_data 
         WHERE building_id = $1 AND period_start = $2 AND period_end = $3`,
        [targetBuildingId, periodStartDate, periodEndDate]
      );
    }

    if (savedReportDataResult.length > 0) {
      const row = savedReportDataResult[0];
      const buildingResult = await query('SELECT * FROM buildings WHERE id = $1', [targetBuildingId]);
      const building = buildingResult.length > 0 ? buildingResult[0] : null;
      const buildingData = building?.data || {};
      const buildingAddress = buildingData?.passport?.address || buildingData?.passport?.general?.address || 'Адрес не указан';
      const companyResult = await query('SELECT * FROM company_settings WHERE id = 1 LIMIT 1');
      const company = companyResult.length > 0 ? companyResult[0] : null;

      // NPS всегда подтягиваем актуально из nps_responses/nps_building_stats, чтобы новые ответы опроса попадали в отчёт
      let npsFromCache = {
        score: parseInt(row.nps_score) || 0,
        avgScore: parseFloat(row.nps_avg_score) || 0,
        totalResponses: parseInt(row.nps_total_responses) || 0,
        promoters: parseInt(row.nps_promoters) || 0,
        passives: parseInt(row.nps_passives) || 0,
        detractors: parseInt(row.nps_detractors) || 0
      };
      const npsStatsRow = await query(
        `SELECT total_responses, nps_score, avg_score, promoters, passives, detractors
         FROM nps_building_stats
         WHERE building_id = $1 AND period_start = $2 AND period_end = $3`,
        [targetBuildingId, periodStartDate, periodEndDate]
      );
      if (npsStatsRow.length > 0 && parseInt(npsStatsRow[0].total_responses) > 0) {
        const r = npsStatsRow[0];
        npsFromCache = {
          score: parseInt(r.nps_score) || 0,
          avgScore: parseFloat(r.avg_score) || 0,
          totalResponses: parseInt(r.total_responses) || 0,
          promoters: parseInt(r.promoters) || 0,
          passives: parseInt(r.passives) || 0,
          detractors: parseInt(r.detractors) || 0
        };
      } else {
        const npsLiveResult = await query(
          `SELECT COUNT(*) as total_responses, AVG(score)::numeric(3,1) as avg_score,
                  COUNT(*) FILTER (WHERE score >= 9) as promoters,
                  COUNT(*) FILTER (WHERE score >= 7 AND score <= 8) as passives,
                  COUNT(*) FILTER (WHERE score <= 6) as detractors
           FROM nps_responses
           WHERE building_id = $1 AND (created_at::date >= $2::date AND created_at::date <= $3::date)`,
          [targetBuildingId, periodStartDate, periodEndDate]
        );
        if (npsLiveResult.length > 0 && parseInt(npsLiveResult[0].total_responses) > 0) {
          const r = npsLiveResult[0];
          const total = parseInt(r.total_responses) || 0;
          const promoters = parseInt(r.promoters) || 0;
          const passives = parseInt(r.passives) || 0;
          const detractors = parseInt(r.detractors) || 0;
          const score = total > 0 ? Math.round((promoters / total * 100) - (detractors / total * 100)) : 0;
          npsFromCache = {
            score,
            avgScore: parseFloat(r.avg_score) || 0,
            totalResponses: total,
            promoters,
            passives,
            detractors
          };
          const promoterPct = total > 0 ? (promoters / total * 100).toFixed(2) : '0';
          const detractorPct = total > 0 ? (detractors / total * 100).toFixed(2) : '0';
          await query(
            `INSERT INTO nps_building_stats 
             (building_id, period_start, period_end, total_responses, nps_score, avg_score, promoters, passives, detractors, promoter_percent, detractor_percent, updated_at)
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())
             ON CONFLICT (building_id, period_start, period_end) 
             DO UPDATE SET total_responses = $4, nps_score = $5, avg_score = $6, promoters = $7, passives = $8, detractors = $9, promoter_percent = $10, detractor_percent = $11, updated_at = NOW()`,
            [targetBuildingId, periodStartDate, periodEndDate, total, score, parseFloat(r.avg_score) || 0, promoters, passives, detractors, promoterPct, detractorPct]
          ).catch(err => console.warn('[Report Data] Ошибка сохранения NPS в nps_building_stats:', err.message));
        }
      }

      // Задачи «запланировано / выполнено» всегда из календарного плана дома (building.data.annualPlan) за период
      const REPORT_MONTHS_CACHE = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
      const periodStartFromCache = new Date(row.period_start || periodStartDate);
      const periodYearCache = periodStartFromCache.getFullYear();
      const periodMonthNameCache = REPORT_MONTHS_CACHE[periodStartFromCache.getMonth()];
      const annualPlanCache = buildingData?.annualPlan;
      let tasksTotalCache = parseInt(row.tasks_total) || 0;
      let tasksCompletedCache = parseInt(row.tasks_completed) || 0;
      if (Array.isArray(annualPlanCache) && annualPlanCache.length > 0) {
        const planForPeriodCache = annualPlanCache.filter(
          (item) => (item.year === periodYearCache || item.year === undefined) && (String(item.month || '').trim() === periodMonthNameCache)
        );
        if (planForPeriodCache.length > 0) {
          tasksTotalCache = planForPeriodCache.length;
          tasksCompletedCache = planForPeriodCache.filter((item) => item.status === 'completed').length;
        }
      }

      return res.json({
        building: {
          id: targetBuildingId,
          address: buildingAddress,
          imageUrl: building?.data?.imageUrl || building?.imageUrl || ''
        },
        company: company ? {
          name: company.name,
          fullName: company.full_name,
          address: company.address,
          phone: company.phone,
          email: company.email,
          website: company.website,
          licenseNumber: company.license_number,
          licenseValidUntil: company.license_valid_until,
          logoUrl: company.logo_url
        } : null,
        period: { start: periodStart.toISOString(), end: periodEnd.toISOString() },
        stats: {
          appsQuality: parseInt(row.apps_quality) || 0,
          appsTotal: parseInt(row.apps_total) || 0,
          appsCompleted: parseInt(row.apps_completed) || 0,
          tasksTotal: tasksTotalCache,
          tasksCompleted: tasksCompletedCache,
          fundsCollected: parseFloat(row.funds_collected) || 0,
          fundsSpent: parseFloat(row.funds_spent) || 0,
          fundsBalance: parseFloat(row.funds_balance) || 0,
          debtCasesWon: parseInt(row.debt_cases_won) || 0,
          debtCollected: parseFloat(row.debt_collected) || 0
        },
        expenses: {
          total: parseFloat(row.expenses_total) || 0,
          byCategory: row.expenses_by_category || {},
          details: row.expenses_by_category || {}
        },
        events: row.events || [],
        nps: npsFromCache,
        workPhotos: row.work_photos || [],
        planItems: row.plan_items || []
      });
    }

    // 1. Информация о доме
    const buildingResult = await query('SELECT * FROM buildings WHERE id = $1', [targetBuildingId]);
    const building = buildingResult.length > 0 ? buildingResult[0] : null;
    const buildingData = building?.data || {};
    const buildingAddress = buildingData?.passport?.address || buildingData?.passport?.general?.address || 'Адрес не указан';
    const buildingImage = building?.data?.imageUrl || building?.imageUrl || '';

    // 2. Настройки компании
    const companyResult = await query('SELECT * FROM company_settings WHERE id = 1 LIMIT 1');
    const company = companyResult.length > 0 ? companyResult[0] : null;

    // 3. Качество отработки заявок (completed_at в schema нет — используем updated_at для «время завершения»)
    const appsResult = await query(
      `SELECT 
        COUNT(*) as total,
        COUNT(*) FILTER (WHERE status = 'done') as completed,
        COUNT(*) FILTER (WHERE status = 'done' AND updated_at <= deadline_at) as completed_on_time,
        COUNT(*) FILTER (WHERE status = 'done' AND updated_at > deadline_at) as completed_late
       FROM applications
       WHERE building_id = $1 
         AND created_at::date >= $2::date 
         AND created_at::date <= $3::date`,
      [targetBuildingId, periodStartDate, periodEndDate]
    );
    const appsStats = appsResult.length > 0 ? appsResult[0] : { total: 0, completed: 0, completed_on_time: 0, completed_late: 0 };
    const appsQuality = appsStats.total > 0 ? Math.round((appsStats.completed_on_time / appsStats.total) * 100) : 0;

    // 4. Задачи из календарного плана дома: building.data.annualPlan за период (месяц/год), иначе fallback — applications
    const REPORT_MONTHS = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
    const periodYear = periodStart.getFullYear();
    const periodMonthName = REPORT_MONTHS[periodStart.getMonth()];
    let tasksStats = { total: 0, completed: 0 };
    const annualPlan = buildingData?.annualPlan || (Array.isArray(buildingData?.annualPlan) ? buildingData.annualPlan : []);
    if (Array.isArray(annualPlan) && annualPlan.length > 0) {
      const planForPeriod = annualPlan.filter(
        (item) => (item.year === periodYear || item.year === undefined) && (String(item.month || '').trim() === periodMonthName)
      );
      const completedInPlan = planForPeriod.filter((item) => item.status === 'completed').length;
      tasksStats = { total: planForPeriod.length, completed: completedInPlan };
    }
    if (tasksStats.total === 0) {
      try {
        const tasksResult = await query(
          `SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE status = 'done') as completed
           FROM applications
           WHERE building_id = $1 AND created_at::date >= $2::date AND created_at::date <= $3::date`,
          [targetBuildingId, periodStartDate, periodEndDate]
        );
        if (tasksResult.length > 0) tasksStats = tasksResult[0];
      } catch (e) {
        console.warn('[Report Data] tasks stats fallback:', e.message);
      }
    }

    // 5. Собрано средств (из building_financial_data); сравниваем по датам без времени
    const financesResult = await query(
      `SELECT 
        COALESCE(SUM(total_income), 0) as total_income,
        COALESCE(SUM(total_expenses), 0) as total_expenses,
        COALESCE(SUM(balance), 0) as balance
       FROM building_financial_data
       WHERE building_id = $1 
         AND period_start::date <= $3::date 
         AND period_end::date >= $2::date`,
      [targetBuildingId, periodStartDate, periodEndDate]
    );
    const finances = financesResult.length > 0 ? financesResult[0] : { total_income: 0, total_expenses: 0, balance: 0 };

    // 6. Работа с долгами (колонки category, metadata могут отсутствовать — при ошибке нули)
    let debtStats = { cases_won: 0, debt_collected: 0 };
    try {
      const debtResult = await query(
        `SELECT 
          COUNT(*) as cases_won,
          COALESCE(SUM(CAST(metadata->>'debt_collected' AS NUMERIC)), 0) as debt_collected
         FROM applications
         WHERE building_id = $1 AND category = 'debt_collection' AND status = 'done'
           AND created_at::date >= $2::date AND created_at::date <= $3::date`,
        [targetBuildingId, periodStartDate, periodEndDate]
      );
      if (debtResult.length > 0) debtStats = debtResult[0];
    } catch (e) {
      console.warn('[Report Data] debt stats (column category/metadata may be missing):', e.message);
    }

    // 7. Расходы по категориям (из building_financial_data.expenses_by_items)
    const expensesResult = await query(
      `SELECT expenses_by_items
       FROM building_financial_data
       WHERE building_id = $1 
         AND period_start::date <= $3::date 
         AND period_end::date >= $2::date
       ORDER BY period_start DESC
       LIMIT 1`,
      [targetBuildingId, periodStartDate, periodEndDate]
    );
    
    let expensesByCategory = {};
    if (expensesResult.length > 0 && expensesResult[0].expenses_by_items) {
      expensesByCategory = expensesResult[0].expenses_by_items;
    }

    // Категоризируем расходы
    const categorizedExpenses = {
      utilities: 0,      // Коммунальные услуги
      maintenance: 0,    // Содержание и ремонт
      management: 0,      // Управление и обслуживание
      other: 0           // Прочие
    };

    // У разных УК названия статей в ОСВ отличаются — проверяем по вхождению ключевых слов
    const utilitiesKeywords = ['электроэнергия', 'электричество', 'водоснабжение', 'водоотведение', 'отопление', 'газ', 'коммунальные'];
    const maintenanceKeywords = ['ремонт', 'содержание', 'лифт', 'уборка', 'мусор', 'материалы', 'благоустройство', 'техническое обслуживание', 'инженерное оборудование', 'общедомовое имущество', 'механизированная уборка', 'расходные материалы'];
    const managementKeywords = ['зарплата', 'персонал', 'административ', 'управленческ', 'страхование', 'управление', 'судебные издержки', 'услуги специализирован'];

    Object.entries(expensesByCategory).forEach(([item, amount]) => {
      const itemLower = item.toLowerCase();
      const amountNum = parseFloat(amount) || 0;
      
      if (utilitiesKeywords.some(kw => itemLower.includes(kw))) {
        categorizedExpenses.utilities += amountNum;
      } else if (maintenanceKeywords.some(kw => itemLower.includes(kw))) {
        categorizedExpenses.maintenance += amountNum;
      } else if (managementKeywords.some(kw => itemLower.includes(kw))) {
        categorizedExpenses.management += amountNum;
      } else {
        categorizedExpenses.other += amountNum;
      }
    });

    // 8. Мероприятия — только из модуля PR Мероприятия (pr_events) по дому и периоду
    let events = [];
    try {
      let prEventsResult = [];
      try {
        prEventsResult = await query(
          `SELECT id, title, date, type, category, status, location, attendees_count, budget, short_plan AS "shortPlan", announcement
           FROM pr_events
           WHERE date >= $2::date AND date <= $3::date
             AND (
               location_building_id = $1
               OR EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(location_building_ids, '[]'::jsonb)) AS elem WHERE elem::text = $1)
               OR (location_district_id IS NOT NULL AND location_district_id = (SELECT data->>'districtId' FROM buildings WHERE id = $1 LIMIT 1))
             )
           ORDER BY date DESC
           LIMIT 20`,
          [targetBuildingId, periodStartDate, periodEndDate]
        );
      } catch (qErr) {
        // Если колонки location_building_ids/location_district_id отсутствуют — только по дому и дате
        prEventsResult = await query(
          `SELECT id, title, date, type, category, status, location, attendees_count, budget, short_plan AS "shortPlan", announcement
           FROM pr_events
           WHERE location_building_id = $1 AND date >= $2::date AND date <= $3::date
           ORDER BY date DESC
           LIMIT 20`,
          [targetBuildingId, periodStartDate, periodEndDate]
        );
      }
      events = (prEventsResult || []).map(row => ({
        id: row.id,
        title: row.title || '',
        date: row.date,
        description: (row.shortPlan || row.announcement) || '',
        location: row.location || buildingAddress,
        attendeesCount: row.attendees_count != null ? parseInt(row.attendees_count, 10) : 0
      }));
    } catch (e) {
      console.warn('[Report Data] events (pr_events):', e.message);
    }

    // 9. NPS — сначала из таблицы nps_building_stats (сохраненные цифры), иначе из nps_responses
    let npsStats = { total_responses: 0, avg_score: 0, promoters: 0, passives: 0, detractors: 0 };
    let nps = 0;
    
    const savedStatsResult = await query(
      `SELECT total_responses, nps_score, avg_score, promoters, passives, detractors, promoter_percent, detractor_percent
       FROM nps_building_stats
       WHERE building_id = $1 AND period_start = $2 AND period_end = $3`,
      [targetBuildingId, periodStartDate, periodEndDate]
    );
    
    if (savedStatsResult.length > 0) {
      const row = savedStatsResult[0];
      npsStats = {
        total_responses: parseInt(row.total_responses) || 0,
        avg_score: parseFloat(row.avg_score) || 0,
        promoters: parseInt(row.promoters) || 0,
        passives: parseInt(row.passives) || 0,
        detractors: parseInt(row.detractors) || 0
      };
      nps = parseInt(row.nps_score) || 0;
    }
    
    // Если в nps_building_stats нет данных — считаем из nps_responses (сравнение по дате без времени)
    if (npsStats.total_responses === 0) {
      const npsResult = await query(
        `SELECT 
          COUNT(*) as total_responses,
          AVG(score)::numeric(3,1) as avg_score,
          COUNT(*) FILTER (WHERE score >= 9) as promoters,
          COUNT(*) FILTER (WHERE score >= 7 AND score <= 8) as passives,
          COUNT(*) FILTER (WHERE score <= 6) as detractors
         FROM nps_responses
         WHERE building_id = $1 
           AND (created_at::date >= $2::date AND created_at::date <= $3::date)`,
        [targetBuildingId, periodStartDate, periodEndDate]
      );
      npsStats = npsResult.length > 0 ? npsResult[0] : npsStats;
      nps = npsStats.total_responses > 0 
        ? Math.round((npsStats.promoters / npsStats.total_responses * 100) - (npsStats.detractors / npsStats.total_responses * 100))
        : 0;
      
      // Сохраняем в nps_building_stats для следующих запросов
      if (npsStats.total_responses > 0) {
        const promoterPct = (npsStats.promoters / npsStats.total_responses * 100).toFixed(2);
        const detractorPct = (npsStats.detractors / npsStats.total_responses * 100).toFixed(2);
        await query(
          `INSERT INTO nps_building_stats 
           (building_id, period_start, period_end, total_responses, nps_score, avg_score, promoters, passives, detractors, promoter_percent, detractor_percent, updated_at)
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())
           ON CONFLICT (building_id, period_start, period_end) 
           DO UPDATE SET total_responses = $4, nps_score = $5, avg_score = $6, promoters = $7, passives = $8, detractors = $9, promoter_percent = $10, detractor_percent = $11, updated_at = NOW()`,
          [targetBuildingId, periodStartDate, periodEndDate, npsStats.total_responses, nps, parseFloat(npsStats.avg_score) || 0, npsStats.promoters, npsStats.passives, npsStats.detractors, promoterPct, detractorPct]
        ).catch(err => console.warn('[NPS Stats] Ошибка сохранения в nps_building_stats:', err.message));
      }
    }
    
    // Если всё ещё нет данных — пробуем из сохраненного отчета (content)
    if (npsStats.total_responses === 0 && report && report.content) {
      try {
        const savedContent = typeof report.content === 'string' ? JSON.parse(report.content) : report.content;
        if (savedContent?.nps && savedContent.nps.totalResponses > 0) {
          npsStats = {
            total_responses: savedContent.nps.totalResponses || 0,
            avg_score: savedContent.nps.avgScore || 0,
            promoters: savedContent.nps.promoters || 0,
            passives: savedContent.nps.passives || 0,
            detractors: savedContent.nps.detractors || 0
          };
          nps = savedContent.nps.score || 0;
        }
      } catch (e) {
        console.warn('[NPS Stats] Ошибка при извлечении сохраненных данных:', e);
      }
    }
    
    console.log(`[NPS Stats] Building: ${targetBuildingId}, Period: ${periodStartDate}-${periodEndDate}, Responses: ${npsStats.total_responses}, NPS: ${nps}`);

    // 10. Фото отчеты (из work_photos); task_id может отсутствовать, если миграция add_task_id не применена
    let workPhotos = [];
    try {
      const photosResult = await query(
        `SELECT w.id, w.work_name, w.work_date, w.description, w.photo_before_url, w.photo_after_url, w.task_id,
                a.description AS task_title, a.number AS task_number
         FROM work_photos w
         LEFT JOIN applications a ON (a.id::text = w.task_id OR a.number = w.task_id)
         WHERE w.building_id = $1 
           AND (w.work_date::date >= $2::date AND w.work_date::date <= $3::date)
         ORDER BY w.work_date DESC`,
        [targetBuildingId, periodStartDate, periodEndDate]
      );
      workPhotos = photosResult.map(row => ({
        id: row.id,
        workName: row.work_name,
        workDate: row.work_date,
        description: row.description,
        photoBeforeUrl: row.photo_before_url,
        photoAfterUrl: row.photo_after_url,
        taskId: row.task_id != null ? row.task_id : null,
        taskTitle: (row.task_title || row.task_number) || null
      }));
    } catch (e) {
      try {
        const photosResult = await query(
          `SELECT id, work_name, work_date, description, photo_before_url, photo_after_url
           FROM work_photos
           WHERE building_id = $1 
             AND (work_date::date >= $2::date AND work_date::date <= $3::date)
           ORDER BY work_date DESC`,
          [targetBuildingId, periodStartDate, periodEndDate]
        );
        workPhotos = photosResult.map(row => ({
          id: row.id,
          workName: row.work_name,
          workDate: row.work_date,
          description: row.description,
          photoBeforeUrl: row.photo_before_url,
          photoAfterUrl: row.photo_after_url,
          taskId: null,
          taskTitle: null
        }));
      } catch (e2) {
        console.warn('[Report Data] work_photos:', e2.message);
      }
    }

    // 11. План работ на следующий месяц (из applications с будущими датами)
    const nextMonthStart = new Date(periodEnd);
    nextMonthStart.setMonth(nextMonthStart.getMonth() + 1);
    nextMonthStart.setDate(1);
    const nextMonthEnd = new Date(nextMonthStart.getFullYear(), nextMonthStart.getMonth() + 1, 0);

    // План работ: в applications нет title, category — используем description
    let planItems = [];
    try {
      const planResult = await query(
        `SELECT id, description, deadline_at, status
         FROM applications
         WHERE building_id = $1 
           AND deadline_at >= $2 
           AND deadline_at <= $3
           AND status != 'done'
         ORDER BY deadline_at ASC
         LIMIT 10`,
        [targetBuildingId, nextMonthStart, nextMonthEnd]
      );
      planItems = planResult.map(row => ({
        id: row.id,
        title: row.description || '',
        description: row.description,
        deadline: row.deadline_at,
        category: null,
        status: row.status
      }));
    } catch (e) {
      console.warn('[Report Data] plan items:', e.message);
    }

    // Если актуальных данных нет, пробуем взять из сохраненного отчета
    let savedContent = null;
    if (report && report.content) {
      try {
        savedContent = typeof report.content === 'string' ? JSON.parse(report.content) : report.content;
      } catch (e) {
        console.warn('[Report Data] Ошибка при парсинге сохраненного content:', e);
      }
    }

    // Fallback для finances
    if (finances.total_income === 0 && finances.total_expenses === 0 && savedContent?.finances) {
      console.log(`[Report Data] Используем сохраненные finances из отчета ${report?.id}`);
      finances.total_income = savedContent.finances.collected || 0;
      finances.total_expenses = savedContent.finances.expenses || 0;
      finances.balance = savedContent.finances.balance || 0;
    }

    // Fallback для applications
    if (appsStats.total === 0 && savedContent?.applications) {
      console.log(`[Report Data] Используем сохраненные applications из отчета ${report?.id}`);
      appsStats.total = savedContent.applications.total || 0;
      appsStats.completed = savedContent.applications.completed || 0;
      appsStats.completed_on_time = Math.round((savedContent.applications.quality || 0) / 100 * (savedContent.applications.total || 0));
      appsQuality = savedContent.applications.quality || 0;
    }

    // Формируем ответ
    const payload = {
      building: {
        id: targetBuildingId,
        address: buildingAddress,
        imageUrl: buildingImage
      },
      company: company ? {
        name: company.name,
        fullName: company.full_name,
        address: company.address,
        phone: company.phone,
        email: company.email,
        website: company.website,
        licenseNumber: company.license_number,
        licenseValidUntil: company.license_valid_until,
        logoUrl: company.logo_url
      } : null,
      period: {
        start: periodStart.toISOString(),
        end: periodEnd.toISOString()
      },
      stats: {
        appsQuality: appsQuality,
        appsTotal: parseInt(appsStats.total) || 0,
        appsCompleted: parseInt(appsStats.completed) || 0,
        tasksTotal: parseInt(tasksStats.total) || 0,
        tasksCompleted: parseInt(tasksStats.completed) || 0,
        fundsCollected: parseFloat(finances.total_income) || 0,
        fundsSpent: parseFloat(finances.total_expenses) || 0,
        fundsBalance: parseFloat(finances.balance) || 0,
        debtCasesWon: parseInt(debtStats.cases_won) || 0,
        debtCollected: parseFloat(debtStats.debt_collected) || 0
      },
      expenses: {
        total: parseFloat(finances.total_expenses) || 0,
        byCategory: categorizedExpenses,
        details: expensesByCategory
      },
      events: events,
      nps: {
        score: nps,
        avgScore: parseFloat(npsStats.avg_score) || 0,
        totalResponses: parseInt(npsStats.total_responses) || 0,
        promoters: parseInt(npsStats.promoters) || 0,
        passives: parseInt(npsStats.passives) || 0,
        detractors: parseInt(npsStats.detractors) || 0
      },
      workPhotos: workPhotos,
      planItems: planItems
    };

    // Сохраняем снимок в resident_report_data (дом + период), чтобы данные были по ключу "дом 12, январь"
    const periodMonth = periodStart.getMonth() + 1;
    const expensesTotal = parseFloat(finances.total_expenses) || 0;
    await query(
      `INSERT INTO resident_report_data (
        building_id, period_start, period_end, period_month, period_year,
        nps_score, nps_total_responses, nps_avg_score, nps_promoters, nps_passives, nps_detractors,
        apps_total, apps_completed, apps_quality, tasks_total, tasks_completed,
        funds_collected, funds_spent, funds_balance, debt_cases_won, debt_collected,
        expenses_total, expenses_by_category, events, work_photos, plan_items, snapshot, updated_at
      ) VALUES (
        $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16,
        $17, $18, $19, $20, $21, $22, $23::jsonb, $24::jsonb, $25::jsonb, $26::jsonb, $27::jsonb, NOW()
      ) ON CONFLICT (building_id, period_start, period_end) DO UPDATE SET
        nps_score = EXCLUDED.nps_score, nps_total_responses = EXCLUDED.nps_total_responses, nps_avg_score = EXCLUDED.nps_avg_score,
        nps_promoters = EXCLUDED.nps_promoters, nps_passives = EXCLUDED.nps_passives, nps_detractors = EXCLUDED.nps_detractors,
        apps_total = EXCLUDED.apps_total, apps_completed = EXCLUDED.apps_completed, apps_quality = EXCLUDED.apps_quality,
        tasks_total = EXCLUDED.tasks_total, tasks_completed = EXCLUDED.tasks_completed,
        funds_collected = EXCLUDED.funds_collected, funds_spent = EXCLUDED.funds_spent, funds_balance = EXCLUDED.funds_balance,
        debt_cases_won = EXCLUDED.debt_cases_won, debt_collected = EXCLUDED.debt_collected,
        expenses_total = EXCLUDED.expenses_total, expenses_by_category = EXCLUDED.expenses_by_category,
        events = EXCLUDED.events, work_photos = EXCLUDED.work_photos, plan_items = EXCLUDED.plan_items, snapshot = EXCLUDED.snapshot, updated_at = NOW()`,
      [
        targetBuildingId, periodStartDate, periodEndDate, periodMonth, periodYear,
        nps, parseInt(npsStats.total_responses) || 0, parseFloat(npsStats.avg_score) || 0, parseInt(npsStats.promoters) || 0, parseInt(npsStats.passives) || 0, parseInt(npsStats.detractors) || 0,
        parseInt(appsStats.total) || 0, parseInt(appsStats.completed) || 0, appsQuality, parseInt(tasksStats.total) || 0, parseInt(tasksStats.completed) || 0,
        parseFloat(finances.total_income) || 0, parseFloat(finances.total_expenses) || 0, parseFloat(finances.balance) || 0,
        parseInt(debtStats.cases_won) || 0, parseFloat(debtStats.debt_collected) || 0,
        expensesTotal, JSON.stringify(expensesByCategory), JSON.stringify(events), JSON.stringify(workPhotos), JSON.stringify(planItems), JSON.stringify(payload)
      ]
    ).catch(err => console.warn('[Report Data] Ошибка сохранения в resident_report_data:', err.message));

    res.json(payload);
  } catch (err) {
    console.error('Error fetching report data:', err);
    const msg = (err && err.message) ? err.message : 'Unknown error';
    res.status(500).json({ error: 'Failed to fetch report data: ' + msg, details: msg });
  }
});

// ========= ЮРИДИЧЕСКИЙ ОТДЕЛ: ДОСУДЕБНАЯ РАБОТА =========

// GET /api/legal/debtors - список должников
app.get(`${API_PREFIX}/legal/debtors`, async (req, res) => {
  try {
    const { status, buildingId, search } = req.query;
    let queryText = `
      SELECT 
        id,
        building_id AS "buildingId",
        apartment,
        debtor_name AS "debtorName",
        phone,
        email,
        address,
        debt_amount AS "debtAmount",
        debt_months AS "debtMonths",
        status,
        transferred_from_finance AS "transferredFromFinance",
        created_at AS "createdAt",
        updated_at AS "updatedAt"
      FROM legal_debtors
      WHERE 1=1
    `;
    const params = [];
    
    if (status) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }
    if (buildingId) {
      queryText += ` AND building_id = $${params.length + 1}`;
      params.push(buildingId);
    }
    if (search) {
      queryText += ` AND (
        apartment ILIKE $${params.length + 1} OR 
        debtor_name ILIKE $${params.length + 1} OR 
        address ILIKE $${params.length + 1}
      )`;
      const searchTerm = `%${search}%`;
      params.push(searchTerm, searchTerm, searchTerm);
    }
    
    queryText += ' ORDER BY created_at DESC';
    
    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching legal debtors:', err);
    res.status(500).json({ error: 'Failed to fetch debtors', details: err.message });
  }
});

// POST /api/legal/debtors - создание должника (перемещение из финансов)
app.post(`${API_PREFIX}/legal/debtors`, async (req, res) => {
  try {
    const { buildingId, apartment, debtorName, phone, email, address, debtAmount, debtMonths } = req.body;
    
    if (!apartment || !address || !debtAmount || !debtMonths) {
      return res.status(400).json({ error: 'Missing required fields: apartment, address, debtAmount, debtMonths' });
    }
    
    const result = await query(
      `INSERT INTO legal_debtors 
        (building_id, apartment, debtor_name, phone, email, address, debt_amount, debt_months, transferred_from_finance)
      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
      RETURNING 
        id,
        building_id AS "buildingId",
        apartment,
        debtor_name AS "debtorName",
        phone,
        email,
        address,
        debt_amount AS "debtAmount",
        debt_months AS "debtMonths",
        status,
        transferred_from_finance AS "transferredFromFinance",
        created_at AS "createdAt",
        updated_at AS "updatedAt"`,
      [buildingId || null, apartment, debtorName || null, phone || null, email || null, address, debtAmount, debtMonths, true]
    );
    
    // Автоматически создаем запись досудебной работы
    if (result[0]) {
      await query(
        `INSERT INTO pre_trial_work (debtor_id, status)
         VALUES ($1, 'new')`,
        [result[0].id]
      );
    }

    try {
      await notificationService.createNotificationForResponsibleZone(pool, 'legal', 'debt', {
        type: 'legal_debtor',
        title: 'Новый должник',
        body: (result[0].debtorName || result[0].address || '').toString().slice(0, 150),
        entityType: 'legal_debtor',
        entityId: String(result[0].id),
      });
    } catch (notifErr) {
      console.warn('Notification (legal debtor create):', notifErr.message);
    }
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating legal debtor:', err);
    res.status(500).json({ error: 'Failed to create debtor', details: err.message });
  }
});

// GET /api/legal/pre-trial-work - список досудебных работ
app.get(`${API_PREFIX}/legal/pre-trial-work`, async (req, res) => {
  try {
    // Проверяем существование таблиц
    const tableCheck = await query(
      `SELECT EXISTS (
        SELECT FROM information_schema.tables 
        WHERE table_schema = 'public' 
        AND table_name = 'pre_trial_work'
      )`
    );
    
    if (!tableCheck[0]?.exists) {
      console.log('[pre-trial-work] Таблицы еще не созданы, возвращаем пустой массив');
      return res.json([]);
    }
    
    const { status, assignedTo, search } = req.query;
    let queryText = `
      SELECT 
        pt.id,
        pt.debtor_id AS "debtorId",
        pt.assigned_to AS "assignedTo",
        pt.status,
        pt.promised_payment_date AS "promisedPaymentDate",
        pt.promised_payment_amount AS "promisedPaymentAmount",
        pt.transferred_to_court AS "transferredToCourt",
        pt.court_case_id AS "courtCaseId",
        pt.notes,
        pt.created_at AS "createdAt",
        pt.updated_at AS "updatedAt",
        json_build_object(
          'id', d.id,
          'buildingId', d.building_id,
          'apartment', d.apartment,
          'debtorName', d.debtor_name,
          'phone', d.phone,
          'email', d.email,
          'address', d.address,
          'debtAmount', d.debt_amount,
          'debtMonths', d.debt_months,
          'status', d.status
        ) AS debtor
      FROM pre_trial_work pt
      LEFT JOIN legal_debtors d ON pt.debtor_id = d.id
      WHERE 1=1
    `;
    const params = [];
    
    if (status) {
      queryText += ` AND pt.status = $${params.length + 1}`;
      params.push(status);
    }
    if (assignedTo) {
      queryText += ` AND pt.assigned_to = $${params.length + 1}`;
      params.push(assignedTo);
    }
    if (search) {
      queryText += ` AND (
        d.apartment ILIKE $${params.length + 1} OR 
        d.debtor_name ILIKE $${params.length + 1} OR 
        d.address ILIKE $${params.length + 1}
      )`;
      const searchTerm = `%${search}%`;
      params.push(searchTerm, searchTerm, searchTerm);
    }
    
    queryText += ' ORDER BY pt.updated_at DESC';
    
    const rows = await query(queryText, params);
    
    // Загружаем действия и обещанные оплаты для каждой работы
    const worksWithDetails = await Promise.all(rows.map(async (work) => {
      // Фильтруем работы без должника
      if (!work.debtor || !work.debtor.id) {
        return null;
      }
      
      let actions = [];
      let promisedPayments = [];
      
      try {
        actions = await query(
          `SELECT 
            id,
            work_id AS "workId",
            action_type AS "actionType",
            action_date AS "actionDate",
            performed_by AS "performedBy",
            result,
            notes,
            attachments,
            created_at AS "createdAt"
          FROM pre_trial_actions
          WHERE work_id = $1
          ORDER BY action_date DESC`,
          [work.id]
        ).catch(() => []);
      } catch (err) {
        console.warn('Error loading actions for work', work.id, err);
      }
      
      try {
        promisedPayments = await query(
          `SELECT 
            id,
            work_id AS "workId",
            promised_date AS "promisedDate",
            promised_amount AS "promisedAmount",
            actual_payment_date AS "actualPaymentDate",
            actual_payment_amount AS "actualPaymentAmount",
            is_paid AS "isPaid",
            reminder_sent AS "reminderSent",
            reminder_date AS "reminderDate",
            notes,
            created_at AS "createdAt",
            updated_at AS "updatedAt"
          FROM promised_payments
          WHERE work_id = $1
          ORDER BY promised_date DESC`,
          [work.id]
        ).catch(() => []);
      } catch (err) {
        console.warn('Error loading payments for work', work.id, err);
      }
      
      return {
        ...work,
        debtor: work.debtor,
        actions: actions || [],
        promisedPayments: promisedPayments || []
      };
    }));
    
    // Фильтруем null значения (работы без должника)
    const validWorks = worksWithDetails.filter(w => w !== null && w.debtor);
    
    res.json(validWorks);
  } catch (err) {
    console.error('Error fetching pre-trial work:', err);
    // Возвращаем пустой массив при ошибке для graceful degradation
    res.json([]);
  }
});

// POST /api/legal/pre-trial-work - создание досудебной работы
app.post(`${API_PREFIX}/legal/pre-trial-work`, async (req, res) => {
  try {
    const { debtorId, assignedTo, notes } = req.body;
    
    if (!debtorId) {
      return res.status(400).json({ error: 'Missing required field: debtorId' });
    }
    
    const result = await query(
      `INSERT INTO pre_trial_work (debtor_id, assigned_to, notes)
      VALUES ($1, $2, $3)
      RETURNING 
        id,
        debtor_id AS "debtorId",
        assigned_to AS "assignedTo",
        status,
        promised_payment_date AS "promisedPaymentDate",
        promised_payment_amount AS "promisedPaymentAmount",
        transferred_to_court AS "transferredToCourt",
        court_case_id AS "courtCaseId",
        notes,
        created_at AS "createdAt",
        updated_at AS "updatedAt"`,
      [debtorId, assignedTo || null, notes || null]
    );
    try {
      const row = result[0];
      const opts = { type: 'pre_trial_new', title: 'Новая досудебная работа', body: (notes || '').slice(0, 80) || 'Назначена досудебная работа', entityType: 'pre_trial_work', entityId: String(row.id) };
      if (assignedTo) {
        const userIds = await notificationService.resolveEmployeeNamesToUserIds(pool, [assignedTo]);
        if (userIds.length > 0) {
          await notificationService.createNotificationForUserIds(pool, userIds, opts);
        } else {
          await notificationService.createNotificationForResponsibleZone(pool, 'legal', 'preTrial', opts);
        }
      } else {
        await notificationService.createNotificationForResponsibleZone(pool, 'legal', 'preTrial', opts);
      }
    } catch (notifErr) {
      console.warn('[notifications] pre-trial POST:', notifErr.message);
    }
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating pre-trial work:', err);
    res.status(500).json({ error: 'Failed to create pre-trial work', details: err.message });
  }
});

// PUT /api/legal/pre-trial-work/:id - обновление досудебной работы
app.put(`${API_PREFIX}/legal/pre-trial-work/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { assignedTo, status, notes, promisedPaymentDate, promisedPaymentAmount } = req.body;
    
    const updates = [];
    const params = [];
    let paramIndex = 1;
    
    if (assignedTo !== undefined) {
      updates.push(`assigned_to = $${paramIndex++}`);
      params.push(assignedTo);
    }
    if (status !== undefined) {
      updates.push(`status = $${paramIndex++}`);
      params.push(status);
    }
    if (notes !== undefined) {
      updates.push(`notes = $${paramIndex++}`);
      params.push(notes);
    }
    if (promisedPaymentDate !== undefined) {
      updates.push(`promised_payment_date = $${paramIndex++}`);
      params.push(promisedPaymentDate || null);
    }
    if (promisedPaymentAmount !== undefined) {
      updates.push(`promised_payment_amount = $${paramIndex++}`);
      params.push(promisedPaymentAmount || null);
    }
    
    updates.push(`updated_at = NOW()`);
    params.push(id);
    
    const result = await query(
      `UPDATE pre_trial_work 
      SET ${updates.join(', ')}
      WHERE id = $${paramIndex}
      RETURNING 
        id,
        debtor_id AS "debtorId",
        assigned_to AS "assignedTo",
        status,
        promised_payment_date AS "promisedPaymentDate",
        promised_payment_amount AS "promisedPaymentAmount",
        transferred_to_court AS "transferredToCourt",
        court_case_id AS "courtCaseId",
        notes,
        created_at AS "createdAt",
        updated_at AS "updatedAt"`,
      params
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Pre-trial work not found' });
    }
    
    res.json(result[0]);
  } catch (err) {
    console.error('Error updating pre-trial work:', err);
    res.status(500).json({ error: 'Failed to update pre-trial work', details: err.message });
  }
});

// POST /api/legal/pre-trial-work/:id/actions - добавление действия
app.post(`${API_PREFIX}/legal/pre-trial-work/:id/actions`, async (req, res) => {
  try {
    const { id } = req.params;
    const { actionType, actionDate, performedBy, result, notes, attachments } = req.body;
    
    if (!actionType || !performedBy) {
      return res.status(400).json({ error: 'Missing required fields: actionType, performedBy' });
    }
    
    if (!['call', 'letter', 'visit'].includes(actionType)) {
      return res.status(400).json({ error: 'Invalid actionType. Must be: call, letter, or visit' });
    }
    
    const resultRows = await query(
      `INSERT INTO pre_trial_actions (work_id, action_type, action_date, performed_by, result, notes, attachments)
      VALUES ($1, $2, $3, $4, $5, $6, $7)
      RETURNING 
        id,
        work_id AS "workId",
        action_type AS "actionType",
        action_date AS "actionDate",
        performed_by AS "performedBy",
        result,
        notes,
        attachments,
        created_at AS "createdAt"`,
      [id, actionType, actionDate || new Date(), performedBy, result || null, notes || null, attachments || []]
    );
    
    // Обновляем статус работы на "in_progress", если был "new"
    await query(
      `UPDATE pre_trial_work 
      SET status = 'in_progress', updated_at = NOW()
      WHERE id = $1 AND status = 'new'`,
      [id]
    );
    
    res.status(201).json(resultRows[0]);
  } catch (err) {
    console.error('Error creating action:', err);
    res.status(500).json({ error: 'Failed to create action', details: err.message });
  }
});

// POST /api/legal/pre-trial-work/:id/promised-payment - добавление обещанной оплаты
app.post(`${API_PREFIX}/legal/pre-trial-work/:id/promised-payment`, async (req, res) => {
  try {
    const { id } = req.params;
    const { promisedDate, promisedAmount, notes } = req.body;
    
    if (!promisedDate || !promisedAmount) {
      return res.status(400).json({ error: 'Missing required fields: promisedDate, promisedAmount' });
    }
    
    const resultRows = await query(
      `INSERT INTO promised_payments (work_id, promised_date, promised_amount, notes)
      VALUES ($1, $2, $3, $4)
      RETURNING 
        id,
        work_id AS "workId",
        promised_date AS "promisedDate",
        promised_amount AS "promisedAmount",
        actual_payment_date AS "actualPaymentDate",
        actual_payment_amount AS "actualPaymentAmount",
        is_paid AS "isPaid",
        reminder_sent AS "reminderSent",
        reminder_date AS "reminderDate",
        notes,
        created_at AS "createdAt",
        updated_at AS "updatedAt"`,
      [id, promisedDate, promisedAmount, notes || null]
    );
    
    // Обновляем досудебную работу
    await query(
      `UPDATE pre_trial_work 
      SET 
        status = 'promised_payment',
        promised_payment_date = $1,
        promised_payment_amount = $2,
        updated_at = NOW()
      WHERE id = $3`,
      [promisedDate, promisedAmount, id]
    );
    
    res.status(201).json(resultRows[0]);
  } catch (err) {
    console.error('Error creating promised payment:', err);
    res.status(500).json({ error: 'Failed to create promised payment', details: err.message });
  }
});

// PUT /api/legal/pre-trial-work/:id/transfer-to-court - передача в суд
app.put(`${API_PREFIX}/legal/pre-trial-work/:id/transfer-to-court`, async (req, res) => {
  try {
    const { id } = req.params;
    const { courtCaseId, courtCaseNumber, subject, courtName, judge } = req.body;
    
    // Получаем данные досудебной работы и должника
    const workData = await query(
      `SELECT 
        pt.id,
        pt.debtor_id,
        pt.notes,
        d.debtor_name,
        d.address,
        d.debt_amount,
        d.apartment
      FROM pre_trial_work pt
      JOIN legal_debtors d ON pt.debtor_id = d.id
      WHERE pt.id = $1`,
      [id]
    );
    
    if (workData.length === 0) {
      return res.status(404).json({ error: 'Pre-trial work not found' });
    }
    
    const work = workData[0];
    let createdCourtCaseId = courtCaseId;
    
    // Если судебное дело еще не создано, создаем его автоматически
    if (!courtCaseId) {
      const caseId = `case-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
      const caseNumber = courtCaseNumber || `А40-${Date.now().toString().slice(-6)}/${new Date().getFullYear()}`;
      const caseSubject = subject || `Взыскание задолженности по оплате жилищно-коммунальных услуг. ${work.address}, кв. ${work.apartment}`;
      
      try {
        const courtCaseResult = await query(
          `INSERT INTO legal_court_cases (
            id, case_number, type, role, subject, debtor_name, address, amount,
            court_name, judge, notes, status
          ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'pre_trial')
          RETURNING id`,
          [
            caseId,
            caseNumber,
            'debt_recovery',
            'plaintiff',
            caseSubject,
            work.debtor_name || null,
            work.address,
            work.debt_amount || 0,
            courtName || null,
            judge || null,
            `Автоматически создано при передаче из досудебной работы. ${work.notes || ''}`
          ]
        );
        
        createdCourtCaseId = caseId;
        
        // Записываем в историю
        try {
          await query(
            `INSERT INTO legal_court_case_history (case_id, from_status, to_status, changed_by)
             VALUES ($1, NULL, 'pre_trial', $2)`,
            [caseId, 'System']
          );
        } catch (histErr) {
          console.warn('Failed to save court case history:', histErr);
        }
        
        console.log(`[transfer-to-court] Создано судебное дело ${caseNumber} для досудебной работы ${id}`);
      } catch (courtErr) {
        console.error('Error creating court case:', courtErr);
        // Продолжаем выполнение даже если не удалось создать дело
      }
    }
    
    // Обновляем досудебную работу
    const result = await query(
      `UPDATE pre_trial_work 
      SET 
        status = 'transferred_to_court',
        transferred_to_court = true,
        court_case_id = $1,
        updated_at = NOW()
      WHERE id = $2
      RETURNING 
        id,
        debtor_id AS "debtorId",
        assigned_to AS "assignedTo",
        status,
        promised_payment_date AS "promisedPaymentDate",
        promised_payment_amount AS "promisedPaymentAmount",
        transferred_to_court AS "transferredToCourt",
        court_case_id AS "courtCaseId",
        notes,
        created_at AS "createdAt",
        updated_at AS "updatedAt"`,
      [createdCourtCaseId || null, id]
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Pre-trial work not found' });
    }
    
    // Обновляем статус должника
    await query(
      `UPDATE legal_debtors 
      SET status = 'transferred_to_court', updated_at = NOW()
      WHERE id = (SELECT debtor_id FROM pre_trial_work WHERE id = $1)`,
      [id]
    );
    
    res.json({
      ...result[0],
      courtCaseId: createdCourtCaseId,
      message: createdCourtCaseId ? 'Дело передано в суд и создано судебное дело' : 'Дело передано в суд'
    });
  } catch (err) {
    console.error('Error transferring to court:', err);
    res.status(500).json({ error: 'Failed to transfer to court', details: err.message });
  }
});

// PUT /api/legal/promised-payments/:id/mark-paid - отметить обещанную оплату как выполненную
app.put(`${API_PREFIX}/legal/promised-payments/:id/mark-paid`, async (req, res) => {
  try {
    const { id } = req.params;
    const { actualPaymentDate, actualPaymentAmount } = req.body;
    
    const result = await query(
      `UPDATE promised_payments 
      SET 
        is_paid = true,
        actual_payment_date = $1,
        actual_payment_amount = $2,
        updated_at = NOW()
      WHERE id = $3
      RETURNING 
        id,
        work_id AS "workId",
        promised_date AS "promisedDate",
        promised_amount AS "promisedAmount",
        actual_payment_date AS "actualPaymentDate",
        actual_payment_amount AS "actualPaymentAmount",
        is_paid AS "isPaid",
        reminder_sent AS "reminderSent",
        reminder_date AS "reminderDate",
        notes,
        created_at AS "createdAt",
        updated_at AS "updatedAt"`,
      [actualPaymentDate || new Date(), actualPaymentAmount || null, id]
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Promised payment not found' });
    }
    
    res.json(result[0]);
  } catch (err) {
    console.error('Error marking payment as paid:', err);
    res.status(500).json({ error: 'Failed to mark payment as paid', details: err.message });
  }
});

// ========= ПРОВЕРКА КОНТРАГЕНТОВ ЧЕРЕЗ DADATA =========

// DaData API credentials (fallback из env, если не заданы в панели управления)
const DADATA_API_KEY_ENV = process.env.DADATA_API_KEY || 'e133e91a9124932f30c29bf0b2c88f9f278bb841';
const DADATA_SECRET_ENV = process.env.DADATA_SECRET || '51b46514663c36c314f8983068a1814062c24821';

// Пороги и настройки для расчёта благонадежности (Dadata findById/party)
// Поля Dadata, используемые для риска: state.status, state.code, invalid, founders[].invalidity,
// managers[].invalidity, address.invalidity, management.disqualified, finance.debt/penalty/income/revenue/expense,
// capital.value, licenses[] (valid_to, activities).
const COUNTERPARTY_RISK = {
  MIN_CAPITAL_THRESHOLD_RUB: 10000,  // уставный капитал ниже — средний риск
  LICENSED_OKVED_CODES: []            // коды ОКВЭД, требующие лицензии (если заданы — проверяем наличие действующей лицензии)
};

// POST /api/legal/check-counterparty - проверка контрагента по ИНН через DaData
app.post(`${API_PREFIX}/legal/check-counterparty`, async (req, res) => {
  try {
    const { inn } = req.body;
    
    if (!inn) {
      return res.status(400).json({ error: 'ИНН не указан' });
    }

    // Проверяем формат ИНН (10 или 12 цифр)
    const innRegex = /^\d{10}$|^\d{12}$/;
    if (!innRegex.test(inn)) {
      return res.status(400).json({ error: 'Неверный формат ИНН. Должно быть 10 или 12 цифр' });
    }

    // Учётные данные: из панели управления (integration_settings) или из env
    let apiKey = DADATA_API_KEY_ENV;
    let secret = DADATA_SECRET_ENV;
    try {
      const settingsRows = await query(
        'SELECT enabled, config FROM integration_settings WHERE key = $1',
        ['dadata']
      );
      if (settingsRows.length > 0 && settingsRows[0].enabled) {
        const config = settingsRows[0].config || {};
        if (config.apiKey && config.apiKey.trim()) {
          apiKey = config.apiKey.trim();
        }
        if (config.secret && config.secret.trim()) {
          secret = config.secret.trim();
        }
      }
    } catch (settingsErr) {
      console.warn('[check-counterparty] Не удалось прочитать настройки DaData из БД, используем env:', settingsErr.message);
    }

    // Вызываем DaData API
    const dadataResponse = await fetch('https://suggestions.dadata.ru/suggestions/api/4_1/rs/findById/party', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Authorization': `Token ${apiKey}`,
        'X-Secret': secret
      },
      body: JSON.stringify({ query: inn })
    });

    if (!dadataResponse.ok) {
      const errorText = await dadataResponse.text();
      console.error('DaData API error:', errorText);
      return res.status(dadataResponse.status).json({ 
        error: 'Ошибка при обращении к DaData API',
        details: errorText 
      });
    }

    const dadataData = await dadataResponse.json();

    if (!dadataData.suggestions || dadataData.suggestions.length === 0) {
      return res.status(404).json({ 
        error: 'Организация с указанным ИНН не найдена',
        inn 
      });
    }

    const company = dadataData.suggestions[0].data;

    // Извлечение дополнительных полей Dadata (тариф «Максимальный»)
    const capital = company.capital ? { type: company.capital.type, value: company.capital.value } : null;
    const documents = company.documents || {};
    const smb = documents.smb ? {
      category: documents.smb.category,
      issueDate: documents.smb.issue_date
    } : null;
    const licenses = Array.isArray(company.licenses) ? company.licenses.map(l => ({
      number: l.number,
      series: l.series,
      validFrom: l.valid_from,
      validTo: l.valid_to,
      activities: l.activities || [],
      issueAuthority: l.issue_authority
    })) : [];
    const addressInvalidity = company.address && company.address.invalidity ? company.address.invalidity : null;
    const foundersInvalidity = Array.isArray(company.founders)
      ? company.founders.filter(f => f.invalidity).map(f => ({ name: f.name || f.fio, invalidity: f.invalidity }))
      : [];
    const managersInvalidity = Array.isArray(company.managers)
      ? company.managers.filter(m => m.invalidity).map(m => ({ name: (m.fio && `${m.fio.surname || ''} ${m.fio.name || ''} ${m.fio.patronymic || ''}`.trim()) || m.name, post: m.post, invalidity: m.invalidity }))
      : [];
    const managementDisqualified = company.management && company.management.disqualified === true;

    // Определяем уровень риска на основе данных
    let riskLevel = 'low';
    const riskReasons = [];

    // 1. Статус организации
    if (company.state) {
      if (company.state.status === 'LIQUIDATED') {
        riskLevel = 'high';
        riskReasons.push('Организация ликвидирована');
      } else if (company.state.status === 'LIQUIDATING') {
        riskLevel = 'high';
        riskReasons.push('Организация находится в процессе ликвидации');
      } else if (company.state.status === 'BANKRUPT') {
        riskLevel = 'high';
        riskReasons.push('Организация признана банкротом');
      } else if (company.state.status === 'REORGANIZING') {
        riskLevel = 'medium';
        riskReasons.push('Организация находится в процессе реорганизации');
      }
    }

    // 2. Недостоверность сведений (общая и по категориям)
    if (company.invalid === true) {
      riskLevel = 'high';
      riskReasons.push('Обнаружены недостоверные сведения об организации');
    }
    if (addressInvalidity) {
      if (riskLevel !== 'high') riskLevel = 'high';
      if (!riskReasons.some(r => r.includes('адрес'))) riskReasons.push('Недостоверный адрес (ФНС)');
    }
    if (foundersInvalidity.length > 0) {
      if (riskLevel !== 'high') riskLevel = 'high';
      if (!riskReasons.some(r => r.includes('учредител'))) riskReasons.push('Недостоверные сведения об учредителях');
    }
    if (managersInvalidity.length > 0) {
      if (riskLevel !== 'high') riskLevel = 'high';
      if (!riskReasons.some(r => r.includes('руководител'))) riskReasons.push('Недостоверные сведения о руководителе');
    }

    // 3. Дисквалификация руководителя
    if (managementDisqualified) {
      riskLevel = 'high';
      riskReasons.push('Руководитель дисквалифицирован');
    }

    // 4. Недоимки и штрафы
    if (company.finance) {
      if (company.finance.debt && company.finance.debt > 0) {
        riskLevel = riskLevel === 'high' ? 'high' : 'medium';
        riskReasons.push(`Недоимки по налогам: ${company.finance.debt.toLocaleString()} ₽`);
      }
      if (company.finance.penalty && company.finance.penalty > 0) {
        riskLevel = riskLevel === 'high' ? 'high' : 'medium';
        riskReasons.push(`Налоговые штрафы: ${company.finance.penalty.toLocaleString()} ₽`);
      }
      // 5. Убыточность
      const rev = company.finance.revenue;
      const exp = company.finance.expense;
      if (typeof rev === 'number' && typeof exp === 'number' && exp > rev) {
        riskLevel = riskLevel === 'high' ? 'high' : 'medium';
        riskReasons.push('Компания убыточна по отчётности за год');
      }
    }

    // 6. Низкий уставный капитал (только если поле пришло)
    if (capital && typeof capital.value === 'number' && capital.value < COUNTERPARTY_RISK.MIN_CAPITAL_THRESHOLD_RUB) {
      riskLevel = riskLevel === 'high' ? 'high' : 'medium';
      riskReasons.push(`Низкий уставный капитал: ${capital.value.toLocaleString()} ₽`);
    }

    // 7. Лицензии: при заданном списке лицензируемых ОКВЭД — проверка наличия действующей лицензии
    const mainOkved = company.okved;
    if (COUNTERPARTY_RISK.LICENSED_OKVED_CODES.length > 0 && mainOkved &&
        COUNTERPARTY_RISK.LICENSED_OKVED_CODES.some(code => mainOkved.startsWith(code))) {
      const now = Date.now();
      const hasValidLicense = licenses.some(l => l.validTo != null && new Date(l.validTo).getTime() >= now);
      if (!hasValidLicense) {
        riskLevel = riskLevel === 'high' ? 'high' : 'medium';
        riskReasons.push('Отсутствует или истекла лицензия на основной вид деятельности');
      }
    }

    // Формируем ответ
    const result = {
      inn: company.inn,
      kpp: company.kpp || null,
      ogrn: company.ogrn || null,
      name: company.name?.full_with_opf || company.name?.short_with_opf || company.name?.full || 'Не указано',
      shortName: company.name?.short_with_opf || company.name?.short || null,
      type: company.type, // LEGAL или INDIVIDUAL
      status: company.state?.status || 'UNKNOWN',
      registrationDate: company.state?.registration_date ? new Date(company.state.registration_date).toISOString().split('T')[0] : null,
      liquidationDate: company.state?.liquidation_date ? new Date(company.state.liquidation_date).toISOString().split('T')[0] : null,
      address: company.address?.value || company.address?.unrestricted_value || null,
      okved: company.okved || null,
      okveds: company.okveds || [],
      management: company.management ? {
        name: company.management.name,
        post: company.management.post
      } : null,
      finance: company.finance ? {
        taxSystem: company.finance.tax_system,
        income: company.finance.income,
        revenue: company.finance.revenue,
        expense: company.finance.expense,
        debt: company.finance.debt,
        penalty: company.finance.penalty,
        year: company.finance.year
      } : null,
      authorities: company.authorities ? {
        ftsRegistration: company.authorities.fts_registration,
        ftsReport: company.authorities.fts_report,
        pf: company.authorities.pf,
        sif: company.authorities.sif
      } : null,
      phones: company.phones || [],
      emails: company.emails || [],
      employeeCount: company.employee_count || null,
      capital,
      smb,
      licenses,
      addressInvalidity: addressInvalidity || undefined,
      foundersInvalidity: foundersInvalidity.length ? foundersInvalidity : undefined,
      managersInvalidity: managersInvalidity.length ? managersInvalidity : undefined,
      managementDisqualified: managementDisqualified || undefined,
      riskLevel,
      riskReasons,
      checkedDate: new Date().toISOString(),
      rawData: company // Полные данные для детального просмотра
    };

    // Сохраняем результат проверки в БД
    try {
      const checkId = await query(
        `INSERT INTO counterparty_checks (
          inn, kpp, ogrn, name, short_name, type, status, registration_date, liquidation_date,
          address, okved, okveds, management_name, management_post, finance_data, authorities_data,
          phones, emails, employee_count, risk_level, risk_reasons, raw_data, checked_by
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
        RETURNING id`,
        [
          result.inn,
          result.kpp,
          result.ogrn,
          result.name,
          result.shortName,
          result.type,
          result.status,
          result.registrationDate,
          result.liquidationDate,
          result.address,
          result.okved,
          JSON.stringify(result.okveds || []),
          result.management?.name || null,
          result.management?.post || null,
          JSON.stringify(result.finance || {}),
          JSON.stringify(result.authorities || {}),
          JSON.stringify(result.phones || []),
          JSON.stringify(result.emails || []),
          result.employeeCount,
          result.riskLevel,
          result.riskReasons,
          JSON.stringify(result.rawData || {}),
          req.body.checkedBy || 'System' // Можно передавать из фронтенда
        ]
      );
      result.id = checkId[0].id;
    } catch (saveError) {
      console.error('Error saving counterparty check:', saveError);
      // Не прерываем выполнение, просто логируем ошибку
    }

    res.json(result);
  } catch (err) {
    console.error('Error checking counterparty:', err);
    res.status(500).json({ 
      error: 'Ошибка при проверке контрагента', 
      details: err.message 
    });
  }
});

// GET /api/legal/counterparties - список проверенных контрагентов
app.get(`${API_PREFIX}/legal/counterparties`, async (req, res) => {
  try {
    const { riskLevel, search, limit = 50 } = req.query;
    
    let queryText = `
      SELECT 
        id,
        inn,
        kpp,
        ogrn,
        name,
        short_name AS "shortName",
        type,
        status,
        registration_date AS "registrationDate",
        liquidation_date AS "liquidationDate",
        address,
        okved,
        okveds,
        management_name AS "managementName",
        management_post AS "managementPost",
        finance_data AS "finance",
        authorities_data AS "authorities",
        phones,
        emails,
        employee_count AS "employeeCount",
        risk_level AS "riskLevel",
        risk_reasons AS "riskReasons",
        checked_by AS "checkedBy",
        checked_at AS "checkedDate",
        notes,
        created_at AS "createdAt"
      FROM counterparty_checks
      WHERE 1=1
    `;
    const params = [];
    
    if (riskLevel) {
      queryText += ` AND risk_level = $${params.length + 1}`;
      params.push(riskLevel);
    }
    if (search) {
      queryText += ` AND (
        name ILIKE $${params.length + 1} OR 
        inn ILIKE $${params.length + 1}
      )`;
      const searchTerm = `%${search}%`;
      params.push(searchTerm, searchTerm);
    }
    
    queryText += ` ORDER BY checked_at DESC LIMIT $${params.length + 1}`;
    params.push(parseInt(limit));
    
    const rows = await query(queryText, params);
    
    // Преобразуем JSONB поля
    const formattedRows = rows.map(row => ({
      ...row,
      okveds: row.okveds || [],
      finance: row.finance || null,
      authorities: row.authorities || null,
      phones: row.phones || [],
      emails: row.emails || [],
      riskReasons: row.riskReasons || []
    }));
    
    res.json(formattedRows);
  } catch (err) {
    console.error('Error fetching counterparties:', err);
    res.status(500).json({ error: 'Failed to fetch counterparties', details: err.message });
  }
});

// GET /api/legal/counterparties/:id - получить детальный отчет о проверке
app.get(`${API_PREFIX}/legal/counterparties/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    
    const result = await query(
      `SELECT 
        id,
        inn,
        kpp,
        ogrn,
        name,
        short_name AS "shortName",
        type,
        status,
        registration_date AS "registrationDate",
        liquidation_date AS "liquidationDate",
        address,
        okved,
        okveds,
        management_name AS "managementName",
        management_post AS "managementPost",
        finance_data AS "finance",
        authorities_data AS "authorities",
        phones,
        emails,
        employee_count AS "employeeCount",
        risk_level AS "riskLevel",
        risk_reasons AS "riskReasons",
        raw_data AS "rawData",
        checked_by AS "checkedBy",
        checked_at AS "checkedDate",
        notes,
        created_at AS "createdAt"
      FROM counterparty_checks
      WHERE id = $1`,
      [id]
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Проверка не найдена' });
    }
    
    const check = result[0];
    const raw = check.rawData || {};

    // Извлекаем расширенные поля из raw_data (для отчёта и UI)
    const capital = raw.capital ? { type: raw.capital.type, value: raw.capital.value } : null;
    const documents = raw.documents || {};
    const smb = documents.smb ? { category: documents.smb.category, issueDate: documents.smb.issue_date } : null;
    const licenses = Array.isArray(raw.licenses) ? raw.licenses.map(l => ({
      number: l.number,
      series: l.series,
      validFrom: l.valid_from,
      validTo: l.valid_to,
      activities: l.activities || [],
      issueAuthority: l.issue_authority
    })) : [];
    const addressInvalidity = raw.address && raw.address.invalidity ? raw.address.invalidity : null;
    const foundersInvalidity = Array.isArray(raw.founders)
      ? raw.founders.filter(f => f.invalidity).map(f => ({ name: f.name || f.fio, invalidity: f.invalidity }))
      : [];
    const managersInvalidity = Array.isArray(raw.managers)
      ? raw.managers.filter(m => m.invalidity).map(m => ({
          name: (m.fio && `${(m.fio.surname || '').trim()} ${(m.fio.name || '').trim()} ${(m.fio.patronymic || '').trim()}`.trim()) || m.name,
          post: m.post,
          invalidity: m.invalidity
        }))
      : [];
    const managementDisqualified = raw.management && raw.management.disqualified === true;

    // Преобразуем JSONB поля
    const formattedCheck = {
      ...check,
      okveds: check.okveds || [],
      finance: check.finance || null,
      authorities: check.authorities || null,
      phones: check.phones || [],
      emails: check.emails || [],
      riskReasons: check.riskReasons || [],
      rawData: check.rawData || null,
      capital: capital || undefined,
      smb: smb || undefined,
      licenses: licenses.length ? licenses : undefined,
      addressInvalidity: addressInvalidity || undefined,
      foundersInvalidity: foundersInvalidity.length ? foundersInvalidity : undefined,
      managersInvalidity: managersInvalidity.length ? managersInvalidity : undefined,
      managementDisqualified: managementDisqualified || undefined
    };

    res.json(formattedCheck);
  } catch (err) {
    console.error('Error fetching counterparty check:', err);
    res.status(500).json({ error: 'Failed to fetch counterparty check', details: err.message });
  }
});

// ========= МОДУЛЬ РАЗВИТИЯ: ENDPOINTS =========

/**
 * Создание аудита по дому при попадании объекта воронки на этап «Анализ».
 * Вызывать при создании (POST) или переходе (PUT) на status === 'analysis'.
 * Создаёт запись в development_audits только если для объекта (address/building_id) ещё нет аудита.
 */
async function ensureAuditWhenLandedOnAnalysis(pipelineRow) {
  if (!pipelineRow || pipelineRow.status !== 'analysis') return;
  const buildingId = pipelineRow.building_id || null;
  const address = pipelineRow.address;
  if (!address) return;
  try {
    const existing = await query(
      'SELECT id FROM development_audits WHERE building_id = $1 OR address = $2 LIMIT 1',
      [buildingId, address]
    );
    if (existing.length > 0) return;
    const auditId = `a-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    const wear_percent = 0;
    const complexity_index = 0;
    const projected_margin = 15;
    const calculated_tariff = calculateTariffFromAudit({ wear_percent, complexity_index, projected_margin });
    await query(
      `INSERT INTO development_audits 
       (id, building_id, address, status, wear_percent, roof_condition, basement_condition, calculated_tariff, projected_margin, complexity_index, inspection_data, audit_date, notes)
       VALUES ($1, $2, $3, 'new', $4, $5, $6, $7, $8, $9, $10::jsonb, $11, $12)`,
      [
        auditId,
        buildingId,
        address,
        wear_percent,
        'fair',
        'fair',
        calculated_tariff,
        projected_margin,
        complexity_index,
        JSON.stringify({}),
        new Date().toISOString().split('T')[0],
        'Автоматически создан при попадании объекта на этап «Анализ». Заполните пункты осмотра — износ и тариф рассчитаются автоматически.'
      ]
    );
  } catch (err) {
    console.warn('[ensureAuditWhenLandedOnAnalysis]', err.message);
  }
}

/**
 * Расчёт прогнозного тарифа по данным аудита.
 * Формула: базовая ставка × (1 + коэффициент износа) × (1 + коэффициент сложности) / (1 − маржа).
 * Базовая ставка 28 ₽/м²; износ и сложность увеличивают тариф; маржа — целевая доля прибыли.
 */
function calculateTariffFromAudit(audit) {
  const BASE_TARIFF = 28;
  const wearPercent = Math.max(0, Math.min(100, Number(audit.wear_percent) || 0));
  const complexityIndex = audit.complexity_index != null ? Math.max(0, Math.min(100, Number(audit.complexity_index))) : 50;
  const marginPercent = Math.max(0, Math.min(50, Number(audit.projected_margin) || 15));
  const wearFactor = 1 + (wearPercent / 100) * 0.5;
  const complexityFactor = 1 + (complexityIndex / 100) * 0.2;
  const marginFactor = 1 / (1 - marginPercent / 100);
  const tariff = BASE_TARIFF * wearFactor * complexityFactor * marginFactor;
  return Math.round(tariff * 100) / 100;
}

// ========= PIPELINE (Воронка) =========

// GET /api/development/pipeline - список объектов в воронке
app.get(`${API_PREFIX}/development/pipeline`, async (req, res) => {
  try {
    const { status, search } = req.query;
    let queryText = 'SELECT * FROM development_pipeline WHERE 1=1';
    const params = [];

    if (status) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }

    if (search) {
      const searchParam = `%${search}%`;
      queryText += ` AND (address ILIKE $${params.length + 1} OR manager ILIKE $${params.length + 1})`;
      params.push(searchParam);
    }

    queryText += ' ORDER BY created_at DESC';

    const rows = await query(queryText, params);
    // Преобразуем snake_case в camelCase для фронтенда
    const formatted = rows.map((r) => ({
      id: r.id,
      address: r.address,
      type: r.type,
      floors: r.floors,
      area: parseFloat(r.area) || 0,
      apartments: r.apartments,
      status: r.status,
      probability: r.probability,
      expectedRevenue: parseFloat(r.expected_revenue) || 0,
      manager: r.manager,
      buildingId: r.building_id || null,
      notes: r.notes || null,
    }));
    res.json(formatted);
  } catch (err) {
    console.error('Error fetching pipeline:', err);
    res.status(500).json({ error: 'Failed to fetch pipeline' });
  }
});

// POST /api/development/pipeline - добавить объект
app.post(`${API_PREFIX}/development/pipeline`, async (req, res) => {
  try {
    const { id, address, type, floors, area, apartments, status, probability, expected_revenue, manager, building_id, notes } = req.body;
    const pipelineStatus = status || 'incoming';

    if (!id || !address || !type || !floors || !area || !apartments || !manager) {
      return res.status(400).json({ error: 'Missing required fields' });
    }

    const result = await query(
      `INSERT INTO development_pipeline (id, address, type, floors, area, apartments, status, probability, expected_revenue, manager, building_id, notes)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
       RETURNING *`,
      [id, address, type, floors, area, apartments, pipelineStatus, probability || 0, expected_revenue || 0, manager, building_id || null, notes || null]
    );

    // Автоматически создаем маркетинговую активность для нового объекта
    try {
      await query(
        `INSERT INTO development_marketing_activities (id, address, status, activists_count, meetings_held, ads_distributed)
         VALUES ($1, $2, 'voting', 0, 0, 0)
         ON CONFLICT DO NOTHING`,
        [`m-${id}`, address]
      );
    } catch (err) {
      console.warn('Failed to create marketing activity:', err);
    }

    // При попадании на этап «Анализ» создаём аудит по дому (если ещё нет)
    await ensureAuditWhenLandedOnAnalysis(result[0]);

    // Проверяем условия для автоматического перехода (только для старых статусов)
    try {
      await pipelineAutomation.checkAutoTransition(id);
    } catch (err) {
      console.warn('Failed to check auto transition:', err);
    }

    const r = result[0];
    res.status(201).json({
      id: r.id,
      address: r.address,
      type: r.type,
      floors: r.floors,
      area: parseFloat(r.area) || 0,
      apartments: r.apartments,
      status: r.status,
      probability: r.probability,
      expectedRevenue: parseFloat(r.expected_revenue) || 0,
      manager: r.manager,
      buildingId: r.building_id || null,
      notes: r.notes || null,
    });
  } catch (err) {
    console.error('Error creating pipeline item:', err);
    res.status(500).json({ error: 'Failed to create pipeline item', details: err.message });
  }
});

// PUT /api/development/pipeline/:id - обновить объект
app.put(`${API_PREFIX}/development/pipeline/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const body = req.body;
    const address = body.address;
    const type = body.type;
    const floors = body.floors;
    const area = body.area;
    const apartments = body.apartments;
    const status = body.status;
    const probability = body.probability;
    const expected_revenue = body.expected_revenue !== undefined ? body.expected_revenue : body.expectedRevenue;
    const manager = body.manager;
    const building_id = body.building_id !== undefined ? body.building_id : body.buildingId;
    const notes = body.notes;

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (address !== undefined) { updates.push(`address = $${paramIndex++}`); params.push(address); }
    if (type !== undefined) { updates.push(`type = $${paramIndex++}`); params.push(type); }
    if (floors !== undefined) { updates.push(`floors = $${paramIndex++}`); params.push(floors); }
    if (area !== undefined) { updates.push(`area = $${paramIndex++}`); params.push(area); }
    if (apartments !== undefined) { updates.push(`apartments = $${paramIndex++}`); params.push(apartments); }
    if (status !== undefined) { updates.push(`status = $${paramIndex++}`); params.push(status); }
    if (probability !== undefined) { updates.push(`probability = $${paramIndex++}`); params.push(probability); }
    if (expected_revenue !== undefined) { updates.push(`expected_revenue = $${paramIndex++}`); params.push(expected_revenue); }
    if (manager !== undefined) { updates.push(`manager = $${paramIndex++}`); params.push(manager); }
    if (building_id !== undefined) { updates.push(`building_id = $${paramIndex++}`); params.push(building_id); }
    if (notes !== undefined) { updates.push(`notes = $${paramIndex++}`); params.push(notes); }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    params.push(id);
    const result = await query(
      `UPDATE development_pipeline SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      params
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Pipeline item not found' });
    }

    const r = result[0];
    // При переходе на этап «Анализ» создаём аудит по дому (если ещё нет)
    if (status !== undefined && r.status === 'analysis') {
      await ensureAuditWhenLandedOnAnalysis(r);
    }

    try {
      const opts = { type: 'pipeline_update', title: 'Объект воронки обновлён', body: status !== undefined ? `Статус: ${r.status}. ${(r.address || '').slice(0, 50)}` : (r.address || '').slice(0, 80), entityType: 'pipeline', entityId: String(id) };
      const managerName = r.manager || manager;
      if (managerName) {
        const userIds = await notificationService.resolveEmployeeNamesToUserIds(pool, [managerName]);
        if (userIds.length > 0) {
          await notificationService.createNotificationForUserIds(pool, userIds, opts);
        } else {
          await notificationService.createNotificationForResponsibleZone(pool, 'development', 'pipeline', opts);
        }
      } else {
        await notificationService.createNotificationForResponsibleZone(pool, 'development', 'pipeline', opts);
      }
    } catch (notifErr) {
      console.warn('[notifications] pipeline PUT:', notifErr.message);
    }
    res.json({
      id: r.id,
      address: r.address,
      type: r.type,
      floors: r.floors,
      area: parseFloat(r.area) || 0,
      apartments: r.apartments,
      status: r.status,
      probability: r.probability,
      expectedRevenue: parseFloat(r.expected_revenue) || 0,
      manager: r.manager,
      buildingId: r.building_id || null,
      notes: r.notes || null,
    });
  } catch (err) {
    console.error('Error updating pipeline item:', err);
    res.status(500).json({ error: 'Failed to update pipeline item', details: err.message });
  }
});

// DELETE /api/development/pipeline/:id - удалить объект
app.delete(`${API_PREFIX}/development/pipeline/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM development_pipeline WHERE id = $1 RETURNING *', [id]);

    if (result.length === 0) {
      return res.status(404).json({ error: 'Pipeline item not found' });
    }

    res.json({ success: true, message: 'Pipeline item deleted' });
  } catch (err) {
    console.error('Error deleting pipeline item:', err);
    res.status(500).json({ error: 'Failed to delete pipeline item' });
  }
});

// ========= OSS (Собрания) =========

// GET /api/development/oss - список ОСС
app.get(`${API_PREFIX}/development/oss`, async (req, res) => {
  try {
    const { status, building_id } = req.query;
    let queryText = 'SELECT * FROM development_oss_sessions WHERE 1=1';
    const params = [];

    if (status) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }

    if (building_id) {
      queryText += ` AND building_id = $${params.length + 1}`;
      params.push(building_id);
    }

    queryText += ' ORDER BY start_date DESC, created_at DESC';

    const rows = await query(queryText, params);
    
    // Преобразуем snake_case в camelCase для фронтенда
    const formattedRows = rows.map((row) => ({
      id: row.id,
      buildingId: row.building_id || '',
      address: row.address,
      startDate: row.start_date,
      endDate: row.end_date,
      quorumCurrent: parseFloat(row.quorum_current) || 0,
      quorumTotal: parseFloat(row.quorum_total) || 0,
      status: row.status,
      type: row.type,
      description: row.description || null,
      agendaItems: Array.isArray(row.agenda_items) ? row.agenda_items : (row.agenda_items ? JSON.parse(row.agenda_items) : []),
    }));
    
    res.json(formattedRows);
  } catch (err) {
    console.error('Error fetching OSS sessions:', err);
    res.status(500).json({ error: 'Failed to fetch OSS sessions' });
  }
});

// POST /api/development/oss - создать ОСС
app.post(`${API_PREFIX}/development/oss`, async (req, res) => {
  try {
    const { id, building_id, address, start_date, end_date, quorum_total, status, type, description, agenda_items } = req.body;
    const agendaItemsJson = Array.isArray(agenda_items) ? JSON.stringify(agenda_items) : (agenda_items != null ? (typeof agenda_items === 'string' ? agenda_items : JSON.stringify([])) : '[]');

    if (!id || !address || !start_date || !end_date || !quorum_total || !type) {
      return res.status(400).json({ error: 'Missing required fields' });
    }

    let result;
    try {
      result = await query(
        `INSERT INTO development_oss_sessions (id, building_id, address, start_date, end_date, quorum_total, quorum_current, status, type, description, agenda_items)
         VALUES ($1, $2, $3, $4, $5, $6, 0, $7, $8, $9, $10::jsonb)
         RETURNING *`,
        [id, building_id || null, address, start_date, end_date, quorum_total, status || 'planned', type, description || null, agendaItemsJson]
      );
    } catch (insertErr) {
      if (insertErr.message && insertErr.message.includes('agenda_items')) {
        result = await query(
          `INSERT INTO development_oss_sessions (id, building_id, address, start_date, end_date, quorum_total, quorum_current, status, type, description)
           VALUES ($1, $2, $3, $4, $5, $6, 0, $7, $8, $9)
           RETURNING *`,
          [id, building_id || null, address, start_date, end_date, quorum_total, status || 'planned', type, description || null]
        );
      } else {
        throw insertErr;
      }
    }

    if (result.length === 0) {
      return res.status(500).json({ error: 'Failed to create OSS session' });
    }

    const row = result[0];
    
    // Преобразуем snake_case в camelCase для фронтенда
    const formattedResult = {
      id: row.id,
      buildingId: row.building_id || '',
      address: row.address,
      startDate: row.start_date,
      endDate: row.end_date,
      quorumCurrent: parseFloat(row.quorum_current) || 0,
      quorumTotal: parseFloat(row.quorum_total) || 0,
      status: row.status,
      type: row.type,
      description: row.description || null,
      agendaItems: Array.isArray(row.agenda_items) ? row.agenda_items : (row.agenda_items ? JSON.parse(row.agenda_items) : []),
    };

    try {
      await notificationService.createNotificationForResponsibleZone(pool, 'development', 'oss', {
        type: 'oss',
        title: 'Новая ОСС',
        body: (row.address || row.type || 'ОСС') + (row.start_date ? ' — ' + String(row.start_date).slice(0, 10) : ''),
        entityType: 'oss',
        entityId: String(row.id),
      });
    } catch (notifErr) {
      console.warn('Notification (OSS create):', notifErr.message);
    }
    res.status(201).json(formattedResult);
  } catch (err) {
    console.error('Error creating OSS session:', err);
    res.status(500).json({ error: 'Failed to create OSS session', details: err.message });
  }
});

// PUT /api/development/oss/:id - обновить ОСС
app.put(`${API_PREFIX}/development/oss/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { address, start_date, end_date, quorum_total, quorum_current, status, type, description, agenda_items } = req.body;

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (address !== undefined) { updates.push(`address = $${paramIndex++}`); params.push(address); }
    if (start_date !== undefined) { updates.push(`start_date = $${paramIndex++}`); params.push(start_date); }
    if (end_date !== undefined) { updates.push(`end_date = $${paramIndex++}`); params.push(end_date); }
    if (quorum_total !== undefined) { updates.push(`quorum_total = $${paramIndex++}`); params.push(quorum_total); }
    if (quorum_current !== undefined) { updates.push(`quorum_current = $${paramIndex++}`); params.push(quorum_current); }
    if (status !== undefined) { updates.push(`status = $${paramIndex++}`); params.push(status); }
    if (type !== undefined) { updates.push(`type = $${paramIndex++}`); params.push(type); }
    if (description !== undefined) { updates.push(`description = $${paramIndex++}`); params.push(description); }
    if (agenda_items !== undefined) {
      const agendaItemsJson = Array.isArray(agenda_items) ? JSON.stringify(agenda_items) : (typeof agenda_items === 'string' ? agenda_items : '[]');
      updates.push(`agenda_items = $${paramIndex++}::jsonb`);
      params.push(agendaItemsJson);
    }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    params.push(id);
    const result = await query(
      `UPDATE development_oss_sessions SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      params
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'OSS session not found' });
    }

    const row = result[0];
    try {
      await notificationService.createNotificationForResponsibleZone(pool, 'development', 'oss', {
        type: 'oss',
        title: 'Изменение ОСС',
        body: (row.address || row.type || 'ОСС') + (row.start_date ? ' — ' + String(row.start_date).slice(0, 10) : ''),
        entityType: 'oss',
        entityId: String(row.id),
      });
    } catch (notifErr) {
      console.warn('Notification (OSS update):', notifErr.message);
    }
    res.json({
      id: row.id,
      buildingId: row.building_id || '',
      address: row.address,
      startDate: row.start_date,
      endDate: row.end_date,
      quorumCurrent: parseFloat(row.quorum_current) || 0,
      quorumTotal: parseFloat(row.quorum_total) || 0,
      status: row.status,
      type: row.type,
      description: row.description || null,
      agendaItems: Array.isArray(row.agenda_items) ? row.agenda_items : (row.agenda_items ? JSON.parse(row.agenda_items) : []),
    });
  } catch (err) {
    console.error('Error updating OSS session:', err);
    res.status(500).json({ error: 'Failed to update OSS session', details: err.message });
  }
});

// PUT /api/development/oss/:id/quorum - обновить кворум
app.put(`${API_PREFIX}/development/oss/:id/quorum`, async (req, res) => {
  try {
    const { id } = req.params;
    const { quorum_current } = req.body;

    if (quorum_current === undefined) {
      return res.status(400).json({ error: 'quorum_current is required' });
    }

    const result = await query(
      `UPDATE development_oss_sessions SET quorum_current = $1 WHERE id = $2 RETURNING *`,
      [quorum_current, id]
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'OSS session not found' });
    }

    const oss = result[0];
    
    // Проверяем, достигнут ли кворум для автоматического завершения
    const quorumPercent = (oss.quorum_current / oss.quorum_total) * 100;
    if (quorumPercent > 50 && oss.status === 'active') {
      // Можно автоматически завершить ОСС, если кворум достигнут
      // Но лучше оставить это на ручное подтверждение
    }

    // Вызываем обработчик автоматизации для проверки переходов
    try {
      await pipelineAutomation.handleOSSCompletion(oss.id);
    } catch (autoError) {
      console.warn('Pipeline automation error (non-critical):', autoError);
    }

    res.json(oss);
  } catch (err) {
    console.error('Error updating OSS quorum:', err);
    res.status(500).json({ error: 'Failed to update OSS quorum' });
  }
});

// POST /api/development/oss/:id/ballot - внести бюллетень
app.post(`${API_PREFIX}/development/oss/:id/ballot`, async (req, res) => {
  try {
    const { id } = req.params;
    const { apartment, owner_name, area, vote_result, notes, votes_by_item } = req.body;
    const votesByItemJson = votes_by_item != null && typeof votes_by_item === 'object'
      ? JSON.stringify(votes_by_item)
      : (votes_by_item != null && typeof votes_by_item === 'string' ? votes_by_item : '{}');

    if (!apartment || !area) {
      return res.status(400).json({ error: 'Missing required fields: apartment, area' });
    }

    // Проверяем существование ОСС
    const ossCheck = await query('SELECT id FROM development_oss_sessions WHERE id = $1', [id]);
    if (ossCheck.length === 0) {
      return res.status(404).json({ error: 'OSS session not found' });
    }

    // Добавляем или обновляем запись в реестре
    const existing = await query(
      'SELECT id FROM development_oss_registry WHERE oss_session_id = $1 AND apartment = $2',
      [id, apartment]
    );

    let result;
    const updateWithVotes = () => query(
      `UPDATE development_oss_registry 
       SET owner_name = $1, area = $2, ballot_submitted = TRUE, ballot_date = CURRENT_DATE, vote_result = $3, notes = $4, votes_by_item = $5::jsonb
       WHERE id = $6 RETURNING *`,
      [owner_name || null, area, vote_result || null, notes || null, votesByItemJson, existing[0].id]
    );
    const insertWithVotes = () => query(
      `INSERT INTO development_oss_registry (oss_session_id, apartment, owner_name, area, ballot_submitted, ballot_date, vote_result, notes, votes_by_item)
       VALUES ($1, $2, $3, $4, TRUE, CURRENT_DATE, $5, $6, $7::jsonb) RETURNING *`,
      [id, apartment, owner_name || null, area, vote_result || null, notes || null, votesByItemJson]
    );
    try {
      if (existing.length > 0) {
        result = await updateWithVotes();
      } else {
        result = await insertWithVotes();
      }
    } catch (err) {
      if (err.message && err.message.includes('votes_by_item')) {
        if (existing.length > 0) {
          result = await query(
            `UPDATE development_oss_registry SET owner_name = $1, area = $2, ballot_submitted = TRUE, ballot_date = CURRENT_DATE, vote_result = $3, notes = $4 WHERE id = $5 RETURNING *`,
            [owner_name || null, area, vote_result || null, notes || null, existing[0].id]
          );
        } else {
          result = await query(
            `INSERT INTO development_oss_registry (oss_session_id, apartment, owner_name, area, ballot_submitted, ballot_date, vote_result, notes)
             VALUES ($1, $2, $3, $4, TRUE, CURRENT_DATE, $5, $6) RETURNING *`,
            [id, apartment, owner_name || null, area, vote_result || null, notes || null]
          );
        }
      } else {
        throw err;
      }
    }

    // Обновляем кворум ОСС
    const quorumResult = await query(
      `SELECT COALESCE(SUM(area), 0) as total FROM development_oss_registry WHERE oss_session_id = $1 AND ballot_submitted = TRUE`,
      [id]
    );
    const newQuorum = parseFloat(quorumResult[0].total);

    const updateResult = await query(
      `UPDATE development_oss_sessions SET quorum_current = $1 WHERE id = $2 RETURNING *`,
      [newQuorum, id]
    );

    // Проверяем автоматическое завершение ОСС через триггер
    // Триггер в БД автоматически обработает переход pipeline объекта
    // Также вызываем обработчик автоматизации для дополнительной логики
    try {
      await pipelineAutomation.handleOSSCompletion(id);
    } catch (autoError) {
      console.warn('Pipeline automation error (non-critical):', autoError);
    }

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error submitting ballot:', err);
    res.status(500).json({ error: 'Failed to submit ballot', details: err.message });
  }
});

// POST /api/development/oss/:id/ballots/bulk - массовая загрузка бюллетеней
app.post(`${API_PREFIX}/development/oss/:id/ballots/bulk`, async (req, res) => {
  try {
    const { id } = req.params;
    const { ballots } = req.body;

    if (!Array.isArray(ballots) || ballots.length === 0) {
      return res.status(400).json({ error: 'Missing or empty ballots array' });
    }

    const ossCheck = await query('SELECT id FROM development_oss_sessions WHERE id = $1', [id]);
    if (ossCheck.length === 0) {
      return res.status(404).json({ error: 'OSS session not found' });
    }

    let inserted = 0;
    let updated = 0;

    for (const b of ballots) {
      const apartment = b.apartment != null ? String(b.apartment).trim() : '';
      const area = parseFloat(b.area);
      if (!apartment || isNaN(area) || area <= 0) continue;

      const owner_name = b.owner_name != null ? String(b.owner_name).trim() : null;
      const vote_result = ['for', 'against', 'abstain'].includes(b.vote_result) ? b.vote_result : null;
      const notes = b.notes != null ? String(b.notes).trim() : null;
      const votes_by_item = b.votes_by_item != null && typeof b.votes_by_item === 'object'
        ? JSON.stringify(b.votes_by_item)
        : (b.votes_by_item != null && typeof b.votes_by_item === 'string' ? b.votes_by_item : '{}');

      const existing = await query(
        'SELECT id FROM development_oss_registry WHERE oss_session_id = $1 AND apartment = $2',
        [id, apartment]
      );

      try {
        if (existing.length > 0) {
          await query(
            `UPDATE development_oss_registry 
             SET owner_name = $1, area = $2, ballot_submitted = TRUE, ballot_date = CURRENT_DATE, vote_result = $3, notes = $4, votes_by_item = $5::jsonb
             WHERE id = $6`,
            [owner_name, area, vote_result, notes, votes_by_item, existing[0].id]
          );
          updated += 1;
        } else {
          await query(
            `INSERT INTO development_oss_registry (oss_session_id, apartment, owner_name, area, ballot_submitted, ballot_date, vote_result, notes, votes_by_item)
             VALUES ($1, $2, $3, $4, TRUE, CURRENT_DATE, $5, $6, $7::jsonb)`,
            [id, apartment, owner_name, area, vote_result, notes, votes_by_item]
          );
          inserted += 1;
        }
      } catch (err) {
        if (err.message && err.message.includes('votes_by_item')) {
          if (existing.length > 0) {
            await query(
              `UPDATE development_oss_registry SET owner_name = $1, area = $2, ballot_submitted = TRUE, ballot_date = CURRENT_DATE, vote_result = $3, notes = $4 WHERE id = $5`,
              [owner_name, area, vote_result, notes, existing[0].id]
            );
            updated += 1;
          } else {
            await query(
              `INSERT INTO development_oss_registry (oss_session_id, apartment, owner_name, area, ballot_submitted, ballot_date, vote_result, notes)
               VALUES ($1, $2, $3, $4, TRUE, CURRENT_DATE, $5, $6)`,
              [id, apartment, owner_name, area, vote_result, notes]
            );
            inserted += 1;
          }
        } else {
          throw err;
        }
      }
    }

    const quorumResult = await query(
      `SELECT COALESCE(SUM(area), 0) as total FROM development_oss_registry WHERE oss_session_id = $1 AND ballot_submitted = TRUE`,
      [id]
    );
    const newQuorum = parseFloat(quorumResult[0].total);
    await query(
      `UPDATE development_oss_sessions SET quorum_current = $1 WHERE id = $2`,
      [newQuorum, id]
    );

    try {
      await pipelineAutomation.handleOSSCompletion(id);
    } catch (autoError) {
      console.warn('Pipeline automation error (non-critical):', autoError);
    }

    res.status(201).json({ inserted, updated, total: inserted + updated });
  } catch (err) {
    console.error('Error submitting bulk ballots:', err);
    res.status(500).json({ error: 'Failed to submit bulk ballots', details: err.message });
  }
});

// GET /api/development/oss/:id/registry - получить реестр
app.get(`${API_PREFIX}/development/oss/:id/registry`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query(
      'SELECT * FROM development_oss_registry WHERE oss_session_id = $1 ORDER BY apartment',
      [id]
    );
    
    // Преобразуем snake_case в camelCase для фронтенда
    const formattedRows = rows.map((row) => ({
      id: row.id,
      apartment: row.apartment,
      ownerName: row.owner_name || null,
      area: parseFloat(row.area) || 0,
      ballotSubmitted: row.ballot_submitted || false,
      ballotDate: row.ballot_date || null,
      voteResult: row.vote_result || null,
      notes: row.notes || null,
    }));
    
    res.json(formattedRows);
  } catch (err) {
    console.error('Error fetching OSS registry:', err);
    res.status(500).json({ error: 'Failed to fetch OSS registry' });
  }
});

// ========= AUDITS (Аудиты) =========

/** Нормализация оценки: число 1–5 или старые good/fair/poor → 5/3/1. */
function normalizeRatingToScore(rating) {
  if (rating != null && typeof rating === 'number' && rating >= 1 && rating <= 5) return rating;
  if (rating === 'good') return 5;
  if (rating === 'fair') return 3;
  if (rating === 'poor') return 1;
  return null;
}

/** Среднее по подпунктам категории: только оценки 1–5 (или good→5, fair→3, poor→1), без noAccess/notPresent. */
function calcCategoryAverageFromSubItems(subItems) {
  if (!Array.isArray(subItems)) return null;
  const valid = subItems.filter((s) => normalizeRatingToScore(s.rating) != null && !s.noAccess && !s.notPresent);
  if (valid.length === 0) return null;
  const sum = valid.reduce((a, s) => a + normalizeRatingToScore(s.rating), 0);
  return Math.round((sum / valid.length) * 10) / 10;
}

/** Износ % из inspection_data: средние по пунктам (1–5) → (5 − среднее) / 4 × 100. */
function calcWearPercentFromInspectionData(inspectionData) {
  if (!inspectionData || typeof inspectionData !== 'object') return null;
  const categories = ['roof', 'facade', 'entrances', 'infrastructure', 'yard'];
  const averages = categories
    .map((key) => {
      const cat = inspectionData[key];
      return cat && Array.isArray(cat.subItems) ? calcCategoryAverageFromSubItems(cat.subItems) : null;
    })
    .filter((a) => a != null);
  if (averages.length === 0) return null;
  const avg = averages.reduce((s, a) => s + a, 0) / averages.length;
  return Math.round(((5 - avg) / 4) * 100);
}

/** Индекс сложности (0–100) из inspection_data: та же формула, что и износ — из средних по пунктам осмотра (1–5) → (5 − среднее) / 4 × 100. */
function calcComplexityIndexFromInspectionData(inspectionData) {
  return calcWearPercentFromInspectionData(inspectionData);
}

function formatAuditRow(r) {
  return {
    id: r.id,
    buildingId: r.building_id || null,
    address: r.address,
    status: r.status || 'new',
    wearPercent: r.wear_percent != null ? Number(r.wear_percent) : 0,
    roofCondition: r.roof_condition || 'fair',
    basementCondition: r.basement_condition || 'fair',
    calculatedTariff: parseFloat(r.calculated_tariff) || 0,
    projectedMargin: parseFloat(r.projected_margin) || 0,
    complexityIndex: r.complexity_index != null ? parseFloat(r.complexity_index) : null,
    inspectionData: r.inspection_data || {},
    auditDate: r.audit_date ? String(r.audit_date).split('T')[0] : null,
    auditorName: r.auditor_name || null,
    defectListUrl: r.defect_list_url || null,
    notes: r.notes || null,
  };
}

// GET /api/development/audits - список аудитов
app.get(`${API_PREFIX}/development/audits`, async (req, res) => {
  try {
    const { building_id } = req.query;
    let queryText = 'SELECT * FROM development_audits WHERE 1=1';
    const params = [];

    if (building_id) {
      queryText += ` AND building_id = $${params.length + 1}`;
      params.push(building_id);
    }

    queryText += ' ORDER BY audit_date DESC, created_at DESC';

    const rows = await query(queryText, params);
    res.json(rows.map(formatAuditRow));
  } catch (err) {
    console.error('Error fetching audits:', err);
    res.status(500).json({ error: 'Failed to fetch audits' });
  }
});

// GET /api/development/audits/:id - один аудит (карточка)
app.get(`${API_PREFIX}/development/audits/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const rows = await query('SELECT * FROM development_audits WHERE id = $1', [id]);
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Audit not found' });
    }
    res.json(formatAuditRow(rows[0]));
  } catch (err) {
    console.error('Error fetching audit:', err);
    res.status(500).json({ error: 'Failed to fetch audit' });
  }
});

// POST /api/development/audits - создать аудит
app.post(`${API_PREFIX}/development/audits`, async (req, res) => {
  try {
    const body = req.body;
    const id = body.id;
    const building_id = body.building_id || body.buildingId || null;
    const address = body.address;
    const status = body.status || 'new';
    const inspection_data = body.inspection_data ?? body.inspectionData ?? {};
    const wearFromInspection = calcWearPercentFromInspectionData(inspection_data);
    const complexityFromInspection = calcComplexityIndexFromInspectionData(inspection_data);
    const wear_percent = wearFromInspection != null ? wearFromInspection : (body.wear_percent ?? body.wearPercent ?? 0);
    const complexity_index = complexityFromInspection != null ? complexityFromInspection : (body.complexity_index ?? body.complexityIndex ?? 50);
    const roof_condition = body.roof_condition || body.roofCondition || 'fair';
    const basement_condition = body.basement_condition || body.basementCondition || 'fair';
    const projected_margin = body.projected_margin ?? body.projectedMargin ?? 15;
    const audit_date = body.audit_date || body.auditDate || new Date().toISOString().split('T')[0];
    const auditor_name = body.auditor_name || body.auditorName || null;
    const defect_list_url = body.defect_list_url || body.defectListUrl || null;
    const notes = body.notes || null;

    if (!id || !address) {
      return res.status(400).json({ error: 'Missing required fields: id, address' });
    }

    const calculated_tariff = calculateTariffFromAudit({ wear_percent, complexity_index, projected_margin });

    const result = await query(
      `INSERT INTO development_audits (id, building_id, address, status, wear_percent, roof_condition, basement_condition, calculated_tariff, projected_margin, complexity_index, inspection_data, audit_date, auditor_name, defect_list_url, notes)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12, $13, $14, $15)
       RETURNING *`,
      [id, building_id, address, status, wear_percent, roof_condition, basement_condition, calculated_tariff, projected_margin, complexity_index, JSON.stringify(inspection_data), audit_date, auditor_name, defect_list_url, notes]
    );

    const audit = result[0];
    try {
      const pipelineResult = await query(
        'SELECT id FROM development_pipeline WHERE building_id = $1 OR address = $2 LIMIT 1',
        [building_id, address]
      );
      if (pipelineResult.length > 0) {
        await pipelineAutomation.recalculateProbability(pipelineResult[0].id);
        await pipelineAutomation.checkAutoTransition(pipelineResult[0].id);
      }
    } catch (autoError) {
      console.warn('Pipeline automation error (non-critical):', autoError);
    }

    res.status(201).json(formatAuditRow(audit));
  } catch (err) {
    console.error('Error creating audit:', err);
    res.status(500).json({ error: 'Failed to create audit', details: err.message });
  }
});

// PUT /api/development/audits/:id - обновить аудит
app.put(`${API_PREFIX}/development/audits/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const body = req.body;
    const current = await query('SELECT * FROM development_audits WHERE id = $1', [id]);
    if (current.length === 0) {
      return res.status(404).json({ error: 'Audit not found' });
    }
    const row = current[0];

    const address = body.address !== undefined ? body.address : row.address;
    const status = body.status !== undefined ? body.status : row.status;
    const inspection_data = body.inspection_data !== undefined ? body.inspection_data : body.inspectionData !== undefined ? body.inspectionData : row.inspection_data;
    const wearFromInspection = calcWearPercentFromInspectionData(inspection_data);
    const wear_percent =
      wearFromInspection != null
        ? wearFromInspection
        : body.wear_percent !== undefined
          ? body.wear_percent
          : body.wearPercent !== undefined
            ? body.wearPercent
            : row.wear_percent;
    const roof_condition = body.roof_condition || body.roofCondition || row.roof_condition;
    const basement_condition = body.basement_condition || body.basementCondition || row.basement_condition;
    const projected_margin = body.projected_margin !== undefined ? body.projected_margin : body.projectedMargin !== undefined ? body.projectedMargin : row.projected_margin;
    const complexityFromInspection = calcComplexityIndexFromInspectionData(inspection_data);
    const complexity_index =
      complexityFromInspection != null
        ? complexityFromInspection
        : body.complexity_index !== undefined
          ? body.complexity_index
          : body.complexityIndex !== undefined
            ? body.complexityIndex
            : row.complexity_index;

    const calculated_tariff = calculateTariffFromAudit({ wear_percent, complexity_index, projected_margin });

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (body.address !== undefined) { updates.push(`address = $${paramIndex++}`); params.push(address); }
    if (body.status !== undefined) { updates.push(`status = $${paramIndex++}`); params.push(status); }
    if (body.wear_percent !== undefined || body.wearPercent !== undefined || body.inspection_data !== undefined || body.inspectionData !== undefined) {
      updates.push(`wear_percent = $${paramIndex++}`);
      params.push(wear_percent);
    }
    if (body.roof_condition !== undefined || body.roofCondition !== undefined) { updates.push(`roof_condition = $${paramIndex++}`); params.push(roof_condition); }
    if (body.basement_condition !== undefined || body.basementCondition !== undefined) { updates.push(`basement_condition = $${paramIndex++}`); params.push(basement_condition); }
    updates.push(`calculated_tariff = $${paramIndex++}`); params.push(calculated_tariff);
    if (body.projected_margin !== undefined || body.projectedMargin !== undefined) { updates.push(`projected_margin = $${paramIndex++}`); params.push(projected_margin); }
    if (body.complexity_index !== undefined || body.complexityIndex !== undefined || body.inspection_data !== undefined || body.inspectionData !== undefined) {
      updates.push(`complexity_index = $${paramIndex++}`);
      params.push(complexity_index);
    }
    if (body.inspection_data !== undefined || body.inspectionData !== undefined) { updates.push(`inspection_data = $${paramIndex++}::jsonb`); params.push(JSON.stringify(inspection_data)); }
    if (body.audit_date !== undefined || body.auditDate !== undefined) { updates.push(`audit_date = $${paramIndex++}`); params.push(body.audit_date || body.auditDate); }
    if (body.auditor_name !== undefined || body.auditorName !== undefined) { updates.push(`auditor_name = $${paramIndex++}`); params.push(body.auditor_name ?? body.auditorName); }
    if (body.defect_list_url !== undefined || body.defectListUrl !== undefined) { updates.push(`defect_list_url = $${paramIndex++}`); params.push(body.defect_list_url ?? body.defectListUrl); }
    if (body.notes !== undefined) { updates.push(`notes = $${paramIndex++}`); params.push(body.notes); }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    params.push(id);
    const result = await query(
      `UPDATE development_audits SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      params
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Audit not found' });
    }

    res.json(formatAuditRow(result[0]));
  } catch (err) {
    console.error('Error updating audit:', err);
    res.status(500).json({ error: 'Failed to update audit', details: err.message });
  }
});

// GET /api/development/audits/:id/defect-list - скачать дефектную ведомость
app.get(`${API_PREFIX}/development/audits/:id/defect-list`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('SELECT * FROM development_audits WHERE id = $1', [id]);

    if (result.length === 0) {
      return res.status(404).json({ error: 'Audit not found' });
    }

    const audit = result[0];
    
    // Если есть URL файла, возвращаем его
    if (audit.defect_list_url) {
      return res.json({ url: audit.defect_list_url });
    }

    // Иначе возвращаем данные для генерации PDF на фронтенде
    res.json({
      audit: audit,
      message: 'Defect list URL not available. Use audit data to generate PDF on frontend.'
    });
  } catch (err) {
    console.error('Error fetching defect list:', err);
    res.status(500).json({ error: 'Failed to fetch defect list' });
  }
});

// ========= MARKETING (Маркетинг) =========

// Форматирование строки маркетинговой активности в camelCase для фронтенда
function formatMarketingRow(row) {
  if (!row) return null;
  return {
    id: row.id,
    buildingId: row.building_id || null,
    address: row.address,
    activistsCount: row.activists_count != null ? Number(row.activists_count) : 0,
    meetingsHeld: row.meetings_held != null ? Number(row.meetings_held) : 0,
    adsDistributed: row.ads_distributed != null ? Number(row.ads_distributed) : 0,
    competitor: row.competitor || null,
    status: row.status,
    notes: row.notes || null,
  };
}

// GET /api/development/marketing - список активностей
app.get(`${API_PREFIX}/development/marketing`, async (req, res) => {
  try {
    const { status, building_id } = req.query;
    let queryText = 'SELECT * FROM development_marketing_activities WHERE 1=1';
    const params = [];

    if (status) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }

    if (building_id) {
      queryText += ` AND building_id = $${params.length + 1}`;
      params.push(building_id);
    }

    queryText += ' ORDER BY created_at DESC';

    const rows = await query(queryText, params);
    res.json(rows.map(formatMarketingRow));
  } catch (err) {
    console.error('Error fetching marketing activities:', err);
    res.status(500).json({ error: 'Failed to fetch marketing activities' });
  }
});

// POST /api/development/marketing - создать активность
app.post(`${API_PREFIX}/development/marketing`, async (req, res) => {
  try {
    const { id, building_id, address, activists_count, meetings_held, ads_distributed, competitor, status, notes } = req.body;

    if (!id || !address || !status) {
      return res.status(400).json({ error: 'Missing required fields' });
    }

    const result = await query(
      `INSERT INTO development_marketing_activities (id, building_id, address, activists_count, meetings_held, ads_distributed, competitor, status, notes)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
       RETURNING *`,
      [id, building_id || null, address, activists_count || 0, meetings_held || 0, ads_distributed || 0, competitor || null, status, notes || null]
    );

    res.status(201).json(formatMarketingRow(result[0]));
  } catch (err) {
    console.error('Error creating marketing activity:', err);
    res.status(500).json({ error: 'Failed to create marketing activity', details: err.message });
  }
});

// PUT /api/development/marketing/:id - обновить активность
app.put(`${API_PREFIX}/development/marketing/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { address, activists_count, meetings_held, ads_distributed, competitor, status, notes } = req.body;

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (address !== undefined) { updates.push(`address = $${paramIndex++}`); params.push(address); }
    if (activists_count !== undefined) { updates.push(`activists_count = $${paramIndex++}`); params.push(activists_count); }
    if (meetings_held !== undefined) { updates.push(`meetings_held = $${paramIndex++}`); params.push(meetings_held); }
    if (ads_distributed !== undefined) { updates.push(`ads_distributed = $${paramIndex++}`); params.push(ads_distributed); }
    if (competitor !== undefined) { updates.push(`competitor = $${paramIndex++}`); params.push(competitor); }
    if (status !== undefined) { updates.push(`status = $${paramIndex++}`); params.push(status); }
    if (notes !== undefined) { updates.push(`notes = $${paramIndex++}`); params.push(notes); }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    params.push(id);
    const result = await query(
      `UPDATE development_marketing_activities SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      params
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Marketing activity not found' });
    }

    res.json(formatMarketingRow(result[0]));
  } catch (err) {
    console.error('Error updating marketing activity:', err);
    res.status(500).json({ error: 'Failed to update marketing activity', details: err.message });
  }
});

// PUT /api/development/marketing/:id/metrics - обновить метрики
app.put(`${API_PREFIX}/development/marketing/:id/metrics`, async (req, res) => {
  try {
    const { id } = req.params;
    const { activists_count, meetings_held, ads_distributed } = req.body;

    const updates = [];
    const params = [];
    let paramIndex = 1;

    if (activists_count !== undefined) { updates.push(`activists_count = $${paramIndex++}`); params.push(activists_count); }
    if (meetings_held !== undefined) { updates.push(`meetings_held = $${paramIndex++}`); params.push(meetings_held); }
    if (ads_distributed !== undefined) { updates.push(`ads_distributed = $${paramIndex++}`); params.push(ads_distributed); }

    if (updates.length === 0) {
      return res.status(400).json({ error: 'No metrics to update' });
    }

    params.push(id);
    const result = await query(
      `UPDATE development_marketing_activities SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
      params
    );

    if (result.length === 0) {
      return res.status(404).json({ error: 'Marketing activity not found' });
    }

    const activity = result[0];
    
    // Автоматически пересчитываем probability для связанного pipeline объекта
    try {
      const pipelineResult = await query(
        'SELECT id FROM development_pipeline WHERE building_id = $1 OR address = $2 LIMIT 1',
        [activity.building_id || null, activity.address]
      );
      if (pipelineResult.length > 0) {
        await pipelineAutomation.recalculateProbability(pipelineResult[0].id);
        await pipelineAutomation.checkAutoTransition(pipelineResult[0].id);
      }
    } catch (autoError) {
      console.warn('Pipeline automation error (non-critical):', autoError);
    }

    res.json(formatMarketingRow(activity));
  } catch (err) {
    console.error('Error updating marketing metrics:', err);
    res.status(500).json({ error: 'Failed to update marketing metrics' });
  }
});

// ========= SUMMARY (Сводка) =========

// GET /api/development/summary - KPI метрики и статистика
app.get(`${API_PREFIX}/development/summary`, async (req, res) => {
  try {
    // Прирост фонда (сумма area из pipeline со status='transfer')
    const growthResult = await query(
      `SELECT COALESCE(SUM(area), 0) as growth_m2 FROM development_pipeline WHERE status = 'transfer'`
    );
    const growthM2 = parseFloat(growthResult[0].growth_m2) || 0;

    // Успех ОСС (процент завершенных ОСС с кворумом > 50%)
    const ossResult = await query(
      `SELECT 
        COUNT(*) FILTER (WHERE status = 'completed' AND (quorum_current / NULLIF(quorum_total, 0)) > 0.5) as successful,
        COUNT(*) FILTER (WHERE status = 'completed') as total
       FROM development_oss_sessions`
    );
    const ossSuccessRate = ossResult[0].total > 0 
      ? Math.round((ossResult[0].successful / ossResult[0].total) * 100) 
      : 0;

    // CAC (средняя стоимость привлечения на дом) - упрощенный расчет
    const cacResult = await query(
      `SELECT COALESCE(AVG(expected_revenue), 0) as avg_revenue FROM development_pipeline WHERE status = 'transfer'`
    );
    const cac = parseFloat(cacResult[0].avg_revenue) || 15000;

    // Выручка в воронке (сумма expected_revenue из всех объектов)
    const revenueResult = await query(
      `SELECT COALESCE(SUM(expected_revenue), 0) as potential_revenue FROM development_pipeline`
    );
    const potentialRevenue = parseFloat(revenueResult[0].potential_revenue) || 0;

    res.json({
      growthM2: Math.round(growthM2),
      ossSuccessRate,
      cac: Math.round(cac),
      potentialRevenue: Math.round(potentialRevenue)
    });
  } catch (err) {
    console.error('Error fetching development summary:', err);
    res.status(500).json({ error: 'Failed to fetch development summary' });
  }
});

// ========= LOCATIONS (Карта) =========

// GET /api/development/locations - координаты для карты
app.get(`${API_PREFIX}/development/locations`, async (req, res) => {
  try {
    const { status } = req.query;
    let queryText = 'SELECT * FROM development_building_locations WHERE 1=1';
    const params = [];

    if (status) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }

    queryText += ' ORDER BY created_at DESC';

    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching locations:', err);
    res.status(500).json({ error: 'Failed to fetch locations' });
  }
});

// POST /api/development/locations - добавить/обновить локацию
app.post(`${API_PREFIX}/development/locations`, async (req, res) => {
  try {
    const { building_id, address, latitude, longitude, status, competitor_name } = req.body;

    if (!address || !status) {
      return res.status(400).json({ error: 'Missing required fields: address, status' });
    }

    // Проверяем существование записи
    const existing = building_id 
      ? await query('SELECT id FROM development_building_locations WHERE building_id = $1', [building_id])
      : await query('SELECT id FROM development_building_locations WHERE address = $1', [address]);

    let result;
    if (existing.length > 0 && building_id) {
      // Обновляем существующую запись
      result = await query(
        `UPDATE development_building_locations 
         SET address = $1, latitude = $2, longitude = $3, status = $4, competitor_name = $5
         WHERE building_id = $6
         RETURNING *`,
        [address, latitude || null, longitude || null, status, competitor_name || null, building_id]
      );
    } else {
      // Создаем новую запись
      result = await query(
        `INSERT INTO development_building_locations (building_id, address, latitude, longitude, status, competitor_name)
         VALUES ($1, $2, $3, $4, $5, $6)
         RETURNING *`,
        [building_id || null, address, latitude || null, longitude || null, status, competitor_name || null]
      );
    }

    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating/updating location:', err);
    res.status(500).json({ error: 'Failed to create/update location', details: err.message });
  }
});

// ========= CRON ЗАДАЧИ ДЛЯ АВТОМАТИЗАЦИИ =========

// Ежедневная проверка воронки (каждый день в 9:00)
let dailyCheckInterval = null;

function startDailyPipelineCheck() {
  // Проверяем каждый час (можно изменить на раз в день)
  const CHECK_INTERVAL = 60 * 60 * 1000; // 1 час для тестирования, в продакшене можно 24 часа
  
  dailyCheckInterval = setInterval(async () => {
    try {
      console.log('[Cron] Starting daily pipeline automation check...');
      await pipelineAutomation.dailyCheck();
      console.log('[Cron] Daily pipeline check completed');
    } catch (error) {
      console.error('[Cron] Error in daily pipeline check:', error);
    }
  }, CHECK_INTERVAL);

  // Первый запуск через 5 минут после старта сервера
  setTimeout(async () => {
    try {
      console.log('[Cron] Running initial pipeline automation check...');
      await pipelineAutomation.dailyCheck();
    } catch (error) {
      console.error('[Cron] Error in initial pipeline check:', error);
    }
  }, 5 * 60 * 1000);
}

// Ручной запуск проверки через API
app.post(`${API_PREFIX}/development/pipeline/auto-check`, async (req, res) => {
  try {
    const result = await pipelineAutomation.dailyCheck();
    res.json({ 
      success: true, 
      message: `Проверка завершена: обновлено ${result.updated}, переведено ${result.transitioned}`,
      ...result 
    });
  } catch (err) {
    console.error('Error running pipeline auto-check:', err);
    res.status(500).json({ error: 'Failed to run auto-check', details: err.message });
  }
});

// Пересчет probability для конкретного объекта
app.post(`${API_PREFIX}/development/pipeline/:id/recalculate`, async (req, res) => {
  try {
    const { id } = req.params;
    const newProbability = await pipelineAutomation.recalculateProbability(id);
    const transition = await pipelineAutomation.checkAutoTransition(id);
    
    res.json({ 
      success: true,
      probability: newProbability,
      transition: transition || null
    });
  } catch (err) {
    console.error('Error recalculating probability:', err);
    res.status(500).json({ error: 'Failed to recalculate', details: err.message });
  }
});

// ========= ЮРИДИЧЕСКИЙ ОТДЕЛ: ДОГОВОРЫ =========

// GET /api/legal/contracts - список договоров
app.get(`${API_PREFIX}/legal/contracts`, async (req, res) => {
  try {
    const { status, search, viewMode } = req.query;
    
    let queryText = `
      SELECT 
        id,
        number,
        type,
        counterparty,
        counterparty_inn AS "counterpartyInn",
        amount,
        status,
        start_date AS "startDate",
        end_date AS "endDate",
        auto_prolongation AS "autoProlongation",
        manager,
        has_disagreements AS "hasDisagreements",
        contract_file_url AS "contractFileUrl",
        notes,
        created_at AS "createdAt",
        updated_at AS "updatedAt"
      FROM legal_contracts
      WHERE 1=1
    `;
    const params = [];
    
    if (viewMode === 'archive') {
      queryText += ` AND status = 'archived'`;
    } else if (viewMode === 'active') {
      queryText += ` AND status != 'archived'`;
    }
    
    if (status && status !== 'all') {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }
    
    if (search) {
      queryText += ` AND (
        counterparty ILIKE $${params.length + 1} OR 
        number ILIKE $${params.length + 1} OR
        counterparty_inn ILIKE $${params.length + 1}
      )`;
      const searchTerm = `%${search}%`;
      params.push(searchTerm, searchTerm, searchTerm);
    }
    
    queryText += ' ORDER BY created_at DESC';
    
    const rows = await query(queryText, params);
    res.json(rows);
  } catch (err) {
    console.error('Error fetching contracts:', err);
    // Если таблицы еще не созданы, возвращаем пустой массив
    res.json([]);
  }
});

// POST /api/legal/contracts/upload - загрузка файла договора
app.post(`${API_PREFIX}/legal/contracts/upload`, uploadContract.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'Файл не выбран' });
    }
    const fileUrl = `/uploads/contracts/${req.file.filename}`;
    res.json({ fileUrl });
  } catch (err) {
    console.error('Error uploading contract file:', err);
    res.status(500).json({ error: 'Failed to upload file', details: err.message });
  }
});

// POST /api/legal/contracts - создание договора
app.post(`${API_PREFIX}/legal/contracts`, async (req, res) => {
  try {
    const { 
      id, number, type, counterparty, counterpartyInn, amount, 
      startDate, endDate, autoProlongation, manager, notes 
    } = req.body;
    
    if (!number || !type || !counterparty || !amount || !startDate || !endDate || !manager) {
      return res.status(400).json({ error: 'Missing required fields' });
    }
    
    const contractId = id || `contract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    
    const result = await query(
      `INSERT INTO legal_contracts (
        id, number, type, counterparty, counterparty_inn, amount, 
        start_date, end_date, auto_prolongation, manager, notes, status
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'draft')
      RETURNING 
        id,
        number,
        type,
        counterparty,
        counterparty_inn AS "counterpartyInn",
        amount,
        status,
        start_date AS "startDate",
        end_date AS "endDate",
        auto_prolongation AS "autoProlongation",
        manager,
        has_disagreements AS "hasDisagreements",
        contract_file_url AS "contractFileUrl",
        notes,
        created_at AS "createdAt",
        updated_at AS "updatedAt"`,
      [
        contractId, number, type, counterparty, counterpartyInn || null, amount,
        startDate, endDate, autoProlongation || false, manager, notes || null
      ]
    );
    
    // Записываем в историю
    try {
      await query(
        `INSERT INTO legal_contract_history (contract_id, from_status, to_status, changed_by)
         VALUES ($1, NULL, 'draft', $2)`,
        [contractId, manager]
      );
    } catch (histErr) {
      console.warn('Failed to save contract history:', histErr);
    }
    
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating contract:', err);
    if (err.code === '23505') { // Unique violation
      return res.status(400).json({ error: 'Договор с таким номером уже существует' });
    }
    res.status(500).json({ error: 'Failed to create contract', details: err.message });
  }
});

// PUT /api/legal/contracts/:id - обновление договора
app.put(`${API_PREFIX}/legal/contracts/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const updates = req.body;
    
    const updateFields = [];
    const params = [];
    let paramIndex = 1;
    
    if (updates.status !== undefined) {
      // Получаем текущий статус для истории
      try {
        const current = await query('SELECT status FROM legal_contracts WHERE id = $1', [id]);
        if (current.length > 0 && current[0].status !== updates.status) {
          await query(
            `INSERT INTO legal_contract_history (contract_id, from_status, to_status, changed_by)
             VALUES ($1, $2, $3, $4)`,
            [id, current[0].status, updates.status, updates.changedBy || 'System']
          );
        }
      } catch (histErr) {
        console.warn('Failed to save contract history:', histErr);
      }
      updateFields.push(`status = $${paramIndex++}`);
      params.push(updates.status);
    }
    
    const allowedFields = ['type', 'counterparty', 'counterpartyInn', 'amount', 'startDate', 'endDate', 
                          'autoProlongation', 'manager', 'hasDisagreements', 'contractFileUrl', 'notes'];
    
    for (const field of allowedFields) {
      if (updates[field] !== undefined) {
        const dbField = field === 'counterpartyInn' ? 'counterparty_inn' :
                       field === 'startDate' ? 'start_date' :
                       field === 'endDate' ? 'end_date' :
                       field === 'autoProlongation' ? 'auto_prolongation' :
                       field === 'hasDisagreements' ? 'has_disagreements' :
                       field === 'contractFileUrl' ? 'contract_file_url' :
                       field;
        updateFields.push(`${dbField} = $${paramIndex++}`);
        params.push(updates[field]);
      }
    }
    
    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }
    
    updateFields.push(`updated_at = NOW()`);
    params.push(id);
    
    const result = await query(
      `UPDATE legal_contracts 
      SET ${updateFields.join(', ')}
      WHERE id = $${paramIndex}
      RETURNING 
        id,
        number,
        type,
        counterparty,
        counterparty_inn AS "counterpartyInn",
        amount,
        status,
        start_date AS "startDate",
        end_date AS "endDate",
        auto_prolongation AS "autoProlongation",
        manager,
        has_disagreements AS "hasDisagreements",
        contract_file_url AS "contractFileUrl",
        notes,
        created_at AS "createdAt",
        updated_at AS "updatedAt"`,
      params
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Contract not found' });
    }
    
    res.json(result[0]);
  } catch (err) {
    console.error('Error updating contract:', err);
    res.status(500).json({ error: 'Failed to update contract', details: err.message });
  }
});

// GET /api/legal/contracts/:id/history - история изменений статусов договора
app.get(`${API_PREFIX}/legal/contracts/:id/history`, async (req, res) => {
  try {
    const { id } = req.params;
    const historyRows = await query(
      `SELECT from_status AS "fromStatus", to_status AS "toStatus", changed_by AS "changedBy", created_at AS "createdAt"
       FROM legal_contract_history
       WHERE contract_id = $1
       ORDER BY created_at DESC`,
      [id]
    );
    res.json(historyRows || []);
  } catch (err) {
    console.error('Error fetching contract history:', err);
    res.json([]);
  }
});

// GET /api/legal/contracts/:id - получить договор
app.get(`${API_PREFIX}/legal/contracts/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    
    const result = await query(
      `SELECT 
        id,
        number,
        type,
        counterparty,
        counterparty_inn AS "counterpartyInn",
        amount,
        status,
        start_date AS "startDate",
        end_date AS "endDate",
        auto_prolongation AS "autoProlongation",
        manager,
        has_disagreements AS "hasDisagreements",
        contract_file_url AS "contractFileUrl",
        notes,
        created_at AS "createdAt",
        updated_at AS "updatedAt"
      FROM legal_contracts
      WHERE id = $1`,
      [id]
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Contract not found' });
    }
    
    res.json(result[0]);
  } catch (err) {
    console.error('Error fetching contract:', err);
    res.status(500).json({ error: 'Failed to fetch contract', details: err.message });
  }
});

// ========= ЮРИДИЧЕСКИЙ ОТДЕЛ: СУДЕБНЫЕ ДЕЛА =========

// GET /api/legal/court-cases - список судебных дел
app.get(`${API_PREFIX}/legal/court-cases`, async (req, res) => {
  try {
    const { status, type, role, search } = req.query;
    
    let queryText = `
      SELECT 
        id,
        case_number AS "caseNumber",
        type,
        role,
        subject,
        debtor_name AS "debtorName",
        address,
        amount,
        recovered_amount AS "recoveredAmount",
        amount_at_bailiffs AS "amountAtBailiffs",
        status,
        fssp_status AS "fsspStatus",
        bailiff_name AS "bailiffName",
        fssp_last_action_date AS "fsspLastActionDate",
        next_hearing_date AS "nextHearingDate",
        judge,
        court_name AS "courtName",
        case_file_url AS "caseFileUrl",
        notes,
        enforcement_number AS "enforcementNumber",
        enforcement_start_date AS "enforcementStartDate",
        fssp_stage AS "fsspStage",
        penalty_amount AS "penaltyAmount",
        overdue_since AS "overdueSince",
        created_at AS "createdAt",
        updated_at AS "updatedAt"
      FROM legal_court_cases
      WHERE 1=1
    `;
    const params = [];
    
    if (status) {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }
    if (type) {
      queryText += ` AND type = $${params.length + 1}`;
      params.push(type);
    }
    if (role) {
      queryText += ` AND role = $${params.length + 1}`;
      params.push(role);
    }
    if (search) {
      queryText += ` AND (
        case_number ILIKE $${params.length + 1} OR 
        subject ILIKE $${params.length + 1} OR
        debtor_name ILIKE $${params.length + 1}
      )`;
      const searchTerm = `%${search}%`;
      params.push(searchTerm, searchTerm, searchTerm);
    }
    
    queryText += ' ORDER BY created_at DESC';
    
    const rows = await query(queryText, params);
    res.json(rows.map(r => ({ ...r, id: String(r.id) })));
  } catch (err) {
    console.error('Error fetching court cases:', err);
    // Если таблицы еще не созданы, возвращаем пустой массив
    res.json([]);
  }
});

// POST /api/legal/court-cases - создание судебного дела
app.post(`${API_PREFIX}/legal/court-cases`, async (req, res) => {
  try {
    const { 
      id, caseNumber, type, role, subject, debtorName, address, amount,
      nextHearingDate, judge, courtName, notes, createdBy 
    } = req.body;
    
    if (!caseNumber || !type || !role || !subject || !amount) {
      return res.status(400).json({ error: 'Missing required fields: caseNumber, type, role, subject, amount' });
    }
    
    const caseId = id || `case-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    
    const result = await query(
      `INSERT INTO legal_court_cases (
        id, case_number, type, role, subject, debtor_name, address, amount,
        next_hearing_date, judge, court_name, notes, status
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'pre_trial')
      RETURNING 
        id,
        case_number AS "caseNumber",
        type,
        role,
        subject,
        debtor_name AS "debtorName",
        address,
        amount,
        recovered_amount AS "recoveredAmount",
        amount_at_bailiffs AS "amountAtBailiffs",
        status,
        fssp_status AS "fsspStatus",
        bailiff_name AS "bailiffName",
        fssp_last_action_date AS "fsspLastActionDate",
        next_hearing_date AS "nextHearingDate",
        judge,
        court_name AS "courtName",
        case_file_url AS "caseFileUrl",
        notes,
        created_at AS "createdAt",
        updated_at AS "updatedAt"`,
      [
        caseId, caseNumber, type, role, subject, debtorName || null, address || null, amount,
        nextHearingDate || null, judge || null, courtName || null, notes || null
      ]
    );
    
    // Записываем в историю
    try {
      await query(
        `INSERT INTO legal_court_case_history (case_id, from_status, to_status, changed_by)
         VALUES ($1, NULL, 'pre_trial', $2)`,
        [caseId, createdBy || 'System']
      );
    } catch (histErr) {
      console.warn('Failed to save court case history:', histErr);
    }
    
    res.status(201).json({ ...result[0], id: String(result[0].id) });
  } catch (err) {
    console.error('Error creating court case:', err);
    if (err.code === '23505') { // Unique violation
      return res.status(400).json({ error: 'Судебное дело с таким номером уже существует' });
    }
    res.status(500).json({ error: 'Failed to create court case', details: err.message });
  }
});

// PUT /api/legal/court-cases/:id - обновление судебного дела
app.put(`${API_PREFIX}/legal/court-cases/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const updates = req.body;
    
    const updateFields = [];
    const params = [];
    let paramIndex = 1;
    
    if (updates.status !== undefined) {
      // Получаем текущий статус для истории
      try {
        const current = await query('SELECT status FROM legal_court_cases WHERE id = $1', [id]);
        if (current.length > 0 && current[0].status !== updates.status) {
          await query(
            `INSERT INTO legal_court_case_history (case_id, from_status, to_status, changed_by)
             VALUES ($1, $2, $3, $4)`,
            [id, current[0].status, updates.status, updates.changedBy || 'System']
          );
        }
      } catch (histErr) {
        console.warn('Failed to save court case history:', histErr);
      }
      updateFields.push(`status = $${paramIndex++}`);
      params.push(updates.status);
    }
    
    const allowedFields = ['caseNumber', 'type', 'role', 'subject', 'debtorName', 'address', 'amount',
                          'recoveredAmount', 'amountAtBailiffs', 'fsspStatus', 'bailiffName', 
                          'fsspLastActionDate', 'nextHearingDate', 'judge', 'courtName', 'caseFileUrl', 'notes',
                          'enforcementNumber', 'enforcementStartDate', 'fsspStage', 'penaltyAmount', 'overdueSince'];
    
    for (const field of allowedFields) {
      if (updates[field] !== undefined) {
        const dbField = field === 'caseNumber' ? 'case_number' :
                       field === 'debtorName' ? 'debtor_name' :
                       field === 'recoveredAmount' ? 'recovered_amount' :
                       field === 'amountAtBailiffs' ? 'amount_at_bailiffs' :
                       field === 'fsspStatus' ? 'fssp_status' :
                       field === 'bailiffName' ? 'bailiff_name' :
                       field === 'fsspLastActionDate' ? 'fssp_last_action_date' :
                       field === 'nextHearingDate' ? 'next_hearing_date' :
                       field === 'courtName' ? 'court_name' :
                       field === 'caseFileUrl' ? 'case_file_url' :
                       field === 'enforcementNumber' ? 'enforcement_number' :
                       field === 'enforcementStartDate' ? 'enforcement_start_date' :
                       field === 'fsspStage' ? 'fssp_stage' :
                       field === 'penaltyAmount' ? 'penalty_amount' :
                       field === 'overdueSince' ? 'overdue_since' :
                       field;
        updateFields.push(`${dbField} = $${paramIndex++}`);
        params.push(updates[field]);
      }
    }
    
    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }
    
    updateFields.push(`updated_at = NOW()`);
    params.push(id);
    
    const result = await query(
      `UPDATE legal_court_cases 
      SET ${updateFields.join(', ')}
      WHERE id = $${paramIndex}
      RETURNING 
        id,
        case_number AS "caseNumber",
        type,
        role,
        subject,
        debtor_name AS "debtorName",
        address,
        amount,
        recovered_amount AS "recoveredAmount",
        amount_at_bailiffs AS "amountAtBailiffs",
        status,
        fssp_status AS "fsspStatus",
        bailiff_name AS "bailiffName",
        fssp_last_action_date AS "fsspLastActionDate",
        next_hearing_date AS "nextHearingDate",
        judge,
        court_name AS "courtName",
        case_file_url AS "caseFileUrl",
        notes,
        enforcement_number AS "enforcementNumber",
        enforcement_start_date AS "enforcementStartDate",
        fssp_stage AS "fsspStage",
        penalty_amount AS "penaltyAmount",
        overdue_since AS "overdueSince",
        created_at AS "createdAt",
        updated_at AS "updatedAt"`,
      params
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Court case not found' });
    }
    
    res.json({ ...result[0], id: String(result[0].id) });
  } catch (err) {
    console.error('Error updating court case:', err);
    res.status(500).json({ error: 'Failed to update court case', details: err.message });
  }
});

// GET /api/legal/court-cases/:id - получить судебное дело
app.get(`${API_PREFIX}/legal/court-cases/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    
    const result = await query(
      `SELECT 
        id,
        case_number AS "caseNumber",
        type,
        role,
        subject,
        debtor_name AS "debtorName",
        address,
        amount,
        recovered_amount AS "recoveredAmount",
        amount_at_bailiffs AS "amountAtBailiffs",
        status,
        fssp_status AS "fsspStatus",
        bailiff_name AS "bailiffName",
        fssp_last_action_date AS "fsspLastActionDate",
        next_hearing_date AS "nextHearingDate",
        judge,
        court_name AS "courtName",
        case_file_url AS "caseFileUrl",
        notes,
        enforcement_number AS "enforcementNumber",
        enforcement_start_date AS "enforcementStartDate",
        fssp_stage AS "fsspStage",
        penalty_amount AS "penaltyAmount",
        overdue_since AS "overdueSince",
        created_at AS "createdAt",
        updated_at AS "updatedAt"
      FROM legal_court_cases
      WHERE id = $1`,
      [id]
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Court case not found' });
    }
    
    res.json({ ...result[0], id: String(result[0].id) });
  } catch (err) {
    console.error('Error fetching court case:', err);
    res.status(500).json({ error: 'Failed to fetch court case', details: err.message });
  }
});

// GET /api/legal/court-cases/:id/comments - получить комментарии к делу
app.get(`${API_PREFIX}/legal/court-cases/:id/comments`, async (req, res) => {
  try {
    const { id } = req.params;
    
    // Проверяем существование таблицы
    const tableCheck = await query(
      `SELECT EXISTS (
        SELECT FROM information_schema.tables 
        WHERE table_schema = 'public' 
        AND table_name = 'legal_court_case_comments'
      )`
    );
    
    if (!tableCheck[0]?.exists) {
      return res.json([]);
    }
    
    const result = await query(
      `SELECT 
        id,
        case_id AS "caseId",
        author,
        comment,
        created_at AS "createdAt"
      FROM legal_court_case_comments
      WHERE case_id = $1
      ORDER BY created_at DESC`,
      [id]
    );
    
    res.json(result);
  } catch (err) {
    console.error('Error fetching court case comments:', err);
    res.json([]);
  }
});

// POST /api/legal/court-cases/:id/comments - добавить комментарий к делу
app.post(`${API_PREFIX}/legal/court-cases/:id/comments`, async (req, res) => {
  try {
    const { id } = req.params;
    const { author, comment } = req.body;
    
    if (!author || !comment) {
      return res.status(400).json({ error: 'Missing required fields: author, comment' });
    }
    
    // Проверяем существование таблицы
    const tableCheck = await query(
      `SELECT EXISTS (
        SELECT FROM information_schema.tables 
        WHERE table_schema = 'public' 
        AND table_name = 'legal_court_case_comments'
      )`
    );
    
    if (!tableCheck[0]?.exists) {
      return res.status(503).json({ error: 'Comments table not yet created. Please restart server.' });
    }
    
    const result = await query(
      `INSERT INTO legal_court_case_comments (case_id, author, comment)
       VALUES ($1, $2, $3)
       RETURNING 
        id,
        case_id AS "caseId",
        author,
        comment,
        created_at AS "createdAt"`,
      [id, author, comment]
    );
    
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating court case comment:', err);
    res.status(500).json({ error: 'Failed to create comment', details: err.message });
  }
});

// GET /api/legal/court-cases/:id/history - получить историю изменений статусов
app.get(`${API_PREFIX}/legal/court-cases/:id/history`, async (req, res) => {
  try {
    const { id } = req.params;
    
    // Проверяем существование таблицы
    const tableCheck = await query(
      `SELECT EXISTS (
        SELECT FROM information_schema.tables 
        WHERE table_schema = 'public' 
        AND table_name = 'legal_court_case_history'
      )`
    );
    
    if (!tableCheck[0]?.exists) {
      return res.json([]);
    }
    
    const result = await query(
      `SELECT 
        id,
        case_id AS "caseId",
        from_status AS "fromStatus",
        to_status AS "toStatus",
        changed_by AS "changedBy",
        change_reason AS "changeReason",
        created_at AS "createdAt"
      FROM legal_court_case_history
      WHERE case_id = $1
      ORDER BY created_at DESC`,
      [id]
    );
    
    res.json(result);
  } catch (err) {
    console.error('Error fetching court case history:', err);
    res.json([]);
  }
});

// GET /api/legal/court-cases/:id/documents - список документов по делу
app.get(`${API_PREFIX}/legal/court-cases/:id/documents`, async (req, res) => {
  try {
    const { id } = req.params;
    const tableCheck = await query(
      `SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'legal_case_documents')`
    );
    if (!tableCheck[0]?.exists) {
      return res.json([]);
    }
    const result = await query(
      `SELECT id, case_id AS "caseId", doc_type AS "docType", file_url AS "fileUrl", doc_date AS "docDate", title, created_at AS "createdAt"
       FROM legal_case_documents WHERE case_id = $1 ORDER BY doc_date DESC NULLS LAST, created_at DESC`,
      [id]
    );
    res.json(result);
  } catch (err) {
    console.error('Error fetching case documents:', err);
    res.json([]);
  }
});

// POST /api/legal/court-cases/:id/documents - добавить документ
app.post(`${API_PREFIX}/legal/court-cases/:id/documents`, async (req, res) => {
  try {
    const { id } = req.params;
    const { docType, fileUrl, docDate, title } = req.body;
    if (!docType || !fileUrl) {
      return res.status(400).json({ error: 'Missing required fields: docType, fileUrl' });
    }
    const validTypes = ['pretenzia', 'isk', 'reshenie', 'ispolnitelny_list', 'postanovlenie_ip', 'other'];
    if (!validTypes.includes(docType)) {
      return res.status(400).json({ error: 'Invalid docType' });
    }
    const tableCheck = await query(
      `SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'legal_case_documents')`
    );
    if (!tableCheck[0]?.exists) {
      return res.status(503).json({ error: 'Documents table not yet created.' });
    }
    const result = await query(
      `INSERT INTO legal_case_documents (case_id, doc_type, file_url, doc_date, title)
       VALUES ($1, $2, $3, $4, $5)
       RETURNING id, case_id AS "caseId", doc_type AS "docType", file_url AS "fileUrl", doc_date AS "docDate", title, created_at AS "createdAt"`,
      [id, docType, fileUrl, docDate || null, title || null]
    );
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating case document:', err);
    res.status(500).json({ error: 'Failed to create document', details: err.message });
  }
});

// DELETE /api/legal/court-cases/:id/documents/:docId - удалить документ
app.delete(`${API_PREFIX}/legal/court-cases/:id/documents/:docId`, async (req, res) => {
  try {
    const { id, docId } = req.params;
    const tableCheck = await query(
      `SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'legal_case_documents')`
    );
    if (!tableCheck[0]?.exists) {
      return res.status(404).json({ error: 'Not found' });
    }
    const result = await query('DELETE FROM legal_case_documents WHERE id = $1 AND case_id = $2 RETURNING id', [docId, id]);
    if (result.length === 0) {
      return res.status(404).json({ error: 'Document not found' });
    }
    res.json({ deleted: true, id: docId });
  } catch (err) {
    console.error('Error deleting case document:', err);
    res.status(500).json({ error: 'Failed to delete document', details: err.message });
  }
});

// ========= СЧЕТА НА ОПЛАТУ =========

// Вспомогательная функция для получения ролей пользователя
async function getUserRoles(userId) {
  if (!userId) return [];
  try {
    const result = await query(
      'SELECT role FROM user_roles WHERE user_id = $1',
      [userId]
    );
    return result.map(row => row.role);
  } catch (err) {
    console.error('Error fetching user roles:', err);
    return [];
  }
}

// ========= ПЛАТЕЖНЫЙ КАЛЕНДАРЬ =========

// GET /api/finance/payment-calendar/entries - записи календаря
app.get(`${API_PREFIX}/finance/payment-calendar/entries`, async (req, res) => {
  try {
    const { dateFrom, dateTo, direction, type, limit: limitParam = 500 } = req.query;
    const params = [];
    let where = '1=1';
    if (dateFrom) {
      params.push(dateFrom);
      where += ` AND scheduled_date >= $${params.length}`;
    }
    if (dateTo) {
      params.push(dateTo);
      where += ` AND scheduled_date <= $${params.length}`;
    }
    if (direction && (direction === 'outgoing' || direction === 'incoming')) {
      params.push(direction);
      where += ` AND direction = $${params.length}`;
    }
    if (type && ['invoice', 'manual', 'cash'].includes(type)) {
      params.push(type);
      where += ` AND type = $${params.length}`;
    }
    const limit = Math.min(parseInt(limitParam) || 500, 1000);
    params.push(limit);
    const rows = await query(
      `SELECT id, direction, type, payment_invoice_id AS "paymentInvoiceId", category, description, amount,
        scheduled_date AS "scheduledDate", payment_date AS "paymentDate", probability, currency, is_cash AS "isCash",
        contractor_name AS "contractorName", notes, created_by AS "createdBy", created_at AS "createdAt", updated_at AS "updatedAt"
       FROM payment_calendar_entries WHERE ${where} ORDER BY scheduled_date ASC, id ASC LIMIT $${params.length}`,
      params
    );
    res.json({ entries: rows });
  } catch (err) {
    console.error('Error fetching payment calendar entries:', err);
    res.status(500).json({ error: 'Failed to fetch payment calendar entries', details: err.message });
  }
});

// POST /api/finance/payment-calendar/entries - создание записи
app.post(`${API_PREFIX}/finance/payment-calendar/entries`, async (req, res) => {
  try {
    const {
      createdBy,
      direction,
      type = 'manual',
      paymentInvoiceId,
      category = '',
      description = '',
      amount,
      scheduledDate,
      paymentDate,
      probability = 'confirmed',
      currency = 'RUB',
      isCash = false,
      contractorName = '',
      notes
    } = req.body;
    if (!createdBy || !direction || amount == null || !scheduledDate) {
      return res.status(400).json({ error: 'Missing required: createdBy, direction, amount, scheduledDate' });
    }
    if (direction !== 'outgoing' && direction !== 'incoming') {
      return res.status(400).json({ error: 'direction must be outgoing or incoming' });
    }
    const rows = await query(
      `INSERT INTO payment_calendar_entries (direction, type, payment_invoice_id, category, description, amount, scheduled_date, payment_date, probability, currency, is_cash, contractor_name, notes, created_by)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
       RETURNING id, direction, type, payment_invoice_id AS "paymentInvoiceId", category, description, amount,
         scheduled_date AS "scheduledDate", payment_date AS "paymentDate", probability, currency, is_cash AS "isCash",
         contractor_name AS "contractorName", notes, created_by AS "createdBy", created_at AS "createdAt", updated_at AS "updatedAt"`,
      [
        direction,
        ['invoice', 'manual', 'cash'].includes(type) ? type : 'manual',
        paymentInvoiceId || null,
        category || '',
        description || '',
        parseFloat(amount),
        scheduledDate,
        paymentDate || null,
        ['confirmed', 'high', 'medium', 'low'].includes(probability) ? probability : 'confirmed',
        currency || 'RUB',
        !!isCash,
        contractorName || '',
        notes || null
      ]
    );
    res.status(201).json(rows[0]);
  } catch (err) {
    console.error('Error creating payment calendar entry:', err);
    res.status(500).json({ error: 'Failed to create entry', details: err.message });
  }
});

// PUT /api/finance/payment-calendar/entries/:id - обновление записи
app.put(`${API_PREFIX}/finance/payment-calendar/entries/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const {
      direction, type, category, description, amount, scheduledDate, paymentDate, probability, currency, isCash, contractorName, notes
    } = req.body;
    const updates = [];
    const params = [];
    let idx = 1;
    if (direction !== undefined) { params.push(direction); updates.push(`direction = $${idx++}`); }
    if (type !== undefined) { params.push(type); updates.push(`type = $${idx++}`); }
    if (category !== undefined) { params.push(category); updates.push(`category = $${idx++}`); }
    if (description !== undefined) { params.push(description); updates.push(`description = $${idx++}`); }
    if (amount !== undefined) { params.push(parseFloat(amount)); updates.push(`amount = $${idx++}`); }
    if (scheduledDate !== undefined) { params.push(scheduledDate); updates.push(`scheduled_date = $${idx++}`); }
    if (paymentDate !== undefined) { params.push(paymentDate); updates.push(`payment_date = $${idx++}`); }
    if (probability !== undefined) { params.push(probability); updates.push(`probability = $${idx++}`); }
    if (currency !== undefined) { params.push(currency); updates.push(`currency = $${idx++}`); }
    if (isCash !== undefined) { params.push(!!isCash); updates.push(`is_cash = $${idx++}`); }
    if (contractorName !== undefined) { params.push(contractorName); updates.push(`contractor_name = $${idx++}`); }
    if (notes !== undefined) { params.push(notes); updates.push(`notes = $${idx++}`); }
    if (updates.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }
    updates.push(`updated_at = NOW()`);
    params.push(id);
    const place = params.length;
    const rows = await query(
      `UPDATE payment_calendar_entries SET ${updates.join(', ')} WHERE id = $${place} RETURNING id, direction, type, payment_invoice_id AS "paymentInvoiceId", category, description, amount, scheduled_date AS "scheduledDate", payment_date AS "paymentDate", probability, currency, is_cash AS "isCash", contractor_name AS "contractorName", notes, created_by AS "createdBy", created_at AS "createdAt", updated_at AS "updatedAt"`,
      params
    );
    if (rows.length === 0) return res.status(404).json({ error: 'Entry not found' });
    res.json(rows[0]);
  } catch (err) {
    console.error('Error updating payment calendar entry:', err);
    res.status(500).json({ error: 'Failed to update entry', details: err.message });
  }
});

// DELETE /api/finance/payment-calendar/entries/:id
app.delete(`${API_PREFIX}/finance/payment-calendar/entries/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query('DELETE FROM payment_calendar_entries WHERE id = $1 RETURNING id', [id]);
    if (result.length === 0) return res.status(404).json({ error: 'Entry not found' });
    res.status(204).send();
  } catch (err) {
    console.error('Error deleting payment calendar entry:', err);
    res.status(500).json({ error: 'Failed to delete entry', details: err.message });
  }
});

// GET /api/finance/payment-calendar/categories - статьи доходов/расходов
app.get(`${API_PREFIX}/finance/payment-calendar/categories`, async (req, res) => {
  try {
    const { direction } = req.query;
    let where = 'is_active = true';
    const params = [];
    if (direction && (direction === 'income' || direction === 'expense')) {
      params.push(direction);
      where += ` AND direction = $${params.length}`;
    }
    const rows = await query(
      `SELECT id, name, code, direction, parent_id AS "parentId", is_active AS "isActive", sort_order AS "sortOrder" FROM payment_categories WHERE ${where} ORDER BY sort_order, name`,
      params
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching payment categories:', err);
    res.status(500).json({ error: 'Failed to fetch categories', details: err.message });
  }
});

// GET /api/finance/payment-calendar/summary - сводка (остаток, плановые платежи/поступления)
app.get(`${API_PREFIX}/finance/payment-calendar/summary`, async (req, res) => {
  try {
    const { dateFrom, dateTo } = req.query;
    const params = [];
    let where = '1=1';
    if (dateFrom) { params.push(dateFrom); where += ` AND scheduled_date >= $${params.length}`; }
    if (dateTo) { params.push(dateTo); where += ` AND scheduled_date <= $${params.length}`; }
    const rows = await query(
      `SELECT direction, SUM(amount) AS total FROM payment_calendar_entries WHERE ${where} GROUP BY direction`,
      params
    );
    const outgoing = rows.find(r => r.direction === 'outgoing');
    const incoming = rows.find(r => r.direction === 'incoming');
    res.json({
      plannedOutgoing: parseFloat(outgoing?.total || 0),
      plannedIncoming: parseFloat(incoming?.total || 0),
      dateFrom: dateFrom || null,
      dateTo: dateTo || null
    });
  } catch (err) {
    console.error('Error fetching payment calendar summary:', err);
    res.status(500).json({ error: 'Failed to fetch summary', details: err.message });
  }
});

// GET /api/finance/accounts - банки и кошельки (наличка)
app.get(`${API_PREFIX}/finance/accounts`, async (req, res) => {
  try {
    const { type } = req.query;
    let where = 'is_active = true';
    const params = [];
    if (type && (type === 'bank' || type === 'cash')) {
      params.push(type);
      where += ` AND type = $${params.length}`;
    }
    const rows = await query(
      `SELECT id, type, name, balance, currency, sort_order AS "sortOrder" FROM finance_accounts WHERE ${where} ORDER BY type, sort_order, name`,
      params
    );
    res.json(rows);
  } catch (err) {
    console.error('Error fetching finance accounts:', err);
    res.status(500).json({ error: 'Failed to fetch accounts', details: err.message });
  }
});

// POST /api/finance/accounts - создать счет (банк/кошелек)
app.post(`${API_PREFIX}/finance/accounts`, async (req, res) => {
  try {
    const { type, name, balance = 0, currency = 'RUB', sortOrder = 0 } = req.body;
    if (!type || !name || !['bank', 'cash'].includes(type)) {
      return res.status(400).json({ error: 'type (bank|cash) and name are required' });
    }
    const rows = await query(
      `INSERT INTO finance_accounts (type, name, balance, currency, sort_order) VALUES ($1, $2, $3, $4, $5)
       RETURNING id, type, name, balance, currency, sort_order AS "sortOrder"`,
      [type, name, parseFloat(balance) || 0, currency || 'RUB', parseInt(sortOrder) || 0]
    );
    res.status(201).json(rows[0]);
  } catch (err) {
    console.error('Error creating finance account:', err);
    res.status(500).json({ error: 'Failed to create account', details: err.message });
  }
});

// PUT /api/finance/accounts/:id - обновить счет (баланс, название)
app.put(`${API_PREFIX}/finance/accounts/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const { name, balance, currency, sortOrder } = req.body;
    const updates = [];
    const params = [];
    let idx = 1;
    if (name !== undefined) { params.push(name); updates.push(`name = $${idx++}`); }
    if (balance !== undefined) { params.push(parseFloat(balance)); updates.push(`balance = $${idx++}`); }
    if (currency !== undefined) { params.push(currency); updates.push(`currency = $${idx++}`); }
    if (sortOrder !== undefined) { params.push(parseInt(sortOrder)); updates.push(`sort_order = $${idx++}`); }
    if (updates.length === 0) return res.status(400).json({ error: 'No fields to update' });
    updates.push('updated_at = NOW()');
    params.push(id);
    const rows = await query(
      `UPDATE finance_accounts SET ${updates.join(', ')} WHERE id = $${params.length} RETURNING id, type, name, balance, currency, sort_order AS "sortOrder"`,
      params
    );
    if (rows.length === 0) return res.status(404).json({ error: 'Account not found' });
    res.json(rows[0]);
  } catch (err) {
    console.error('Error updating finance account:', err);
    res.status(500).json({ error: 'Failed to update account', details: err.message });
  }
});

// GET /api/finance/payment-invoices - список счетов (scope=own — только счета, созданные текущим пользователем)
app.get(`${API_PREFIX}/finance/payment-invoices`, async (req, res) => {
  try {
    const { status, purposeType, paymentFormat, search, page = 1, limit = 50, buildingId, scope } = req.query;
    
    let queryText = `
      SELECT 
        id,
        invoice_number AS "invoiceNumber",
        created_by AS "createdBy",
        created_at AS "createdAt",
        updated_at AS "updatedAt",
        purpose_type AS "purposeType",
        purpose_building_ids AS "purposeBuildingIds",
        purpose_district_ids AS "purposeDistrictIds",
        purpose_description AS "purposeDescription",
        purpose_event_id AS "purposeEventId",
        plan_item_id AS "planItemId",
        plan_item_building_id AS "planItemBuildingId",
        payment_format AS "paymentFormat",
        item_type AS "itemType",
        contractor_name AS "contractorName",
        contractor_inn AS "contractorInn",
        service_description AS "serviceDescription",
        service_items AS "serviceItems",
        material_items AS "materialItems",
        total_amount AS "totalAmount",
        distribution_method AS "distributionMethod",
        distribution_data AS "distributionData",
        status,
        current_approver_role AS "currentApproverRole",
        approval_history AS "approvalHistory",
        rejection_reason AS "rejectionReason",
        scheduled_date AS "scheduledDate",
        payment_date AS "paymentDate",
        payment_ref AS "paymentRef",
        is_cash AS "isCash",
        postponed_date AS "postponedDate",
        cancel_reason AS "cancelReason",
        is_completed AS "isCompleted",
        closing_docs_received AS "closingDocsReceived",
        closing_docs_files AS "closingDocsFiles",
        notes,
        file_urls AS "fileUrls"
      FROM payment_invoices
      WHERE 1=1
    `;
    const params = [];
    
    if (status && status !== 'all') {
      queryText += ` AND status = $${params.length + 1}`;
      params.push(status);
    }
    
    if (purposeType && purposeType !== 'all') {
      queryText += ` AND purpose_type = $${params.length + 1}`;
      params.push(purposeType);
    }
    
    if (req.query.purposeEventId) {
      queryText += ` AND purpose_event_id = $${params.length + 1}`;
      params.push(req.query.purposeEventId);
    }
    
    if (buildingId && typeof buildingId === 'string') {
      queryText += ` AND (purpose_building_ids @> $${params.length + 1}::jsonb OR plan_item_building_id = $${params.length + 2})`;
      params.push(JSON.stringify([buildingId]), buildingId);
    }
    
    if (paymentFormat && paymentFormat !== 'all') {
      queryText += ` AND payment_format = $${params.length + 1}`;
      params.push(paymentFormat);
    }
    
    if (search) {
      queryText += ` AND (
        invoice_number ILIKE $${params.length + 1} OR
        contractor_name ILIKE $${params.length + 1} OR
        service_description ILIKE $${params.length + 1}
      )`;
      const searchTerm = `%${search}%`;
      params.push(searchTerm, searchTerm, searchTerm);
    }
    
    if (scope === 'own' && req.user && req.user.employeeId) {
      queryText += ` AND created_by = $${params.length + 1}`;
      params.push(req.user.employeeId);
    }
    
    queryText += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
    params.push(parseInt(limit), (parseInt(page) - 1) * parseInt(limit));
    
    const rows = await query(queryText, params);
    
    // Получаем общее количество для пагинации
    let countQuery = `SELECT COUNT(*) as total FROM payment_invoices WHERE 1=1`;
    const countParams = [];
    
    if (status && status !== 'all') {
      countQuery += ` AND status = $${countParams.length + 1}`;
      countParams.push(status);
    }
    if (purposeType && purposeType !== 'all') {
      countQuery += ` AND purpose_type = $${countParams.length + 1}`;
      countParams.push(purposeType);
    }
    if (req.query.purposeEventId) {
      countQuery += ` AND purpose_event_id = $${countParams.length + 1}`;
      countParams.push(req.query.purposeEventId);
    }
    if (buildingId && typeof buildingId === 'string') {
      countQuery += ` AND (purpose_building_ids @> $${countParams.length + 1}::jsonb OR plan_item_building_id = $${countParams.length + 2})`;
      countParams.push(JSON.stringify([buildingId]), buildingId);
    }
    if (paymentFormat && paymentFormat !== 'all') {
      countQuery += ` AND payment_format = $${countParams.length + 1}`;
      countParams.push(paymentFormat);
    }
    if (search) {
      countQuery += ` AND (
        invoice_number ILIKE $${countParams.length + 1} OR
        contractor_name ILIKE $${countParams.length + 1} OR
        service_description ILIKE $${countParams.length + 1}
      )`;
      const searchTerm = `%${search}%`;
      countParams.push(searchTerm, searchTerm, searchTerm);
    }
    if (scope === 'own' && req.user && req.user.employeeId) {
      countQuery += ` AND created_by = $${countParams.length + 1}`;
      countParams.push(req.user.employeeId);
    }
    
    const countResult = await query(countQuery, countParams);
    const total = parseInt(countResult[0]?.total || 0);
    
    res.json({
      invoices: rows,
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total,
        totalPages: Math.ceil(total / parseInt(limit))
      }
    });
  } catch (err) {
    console.error('Error fetching payment invoices:', err);
    res.status(500).json({ error: 'Failed to fetch payment invoices', details: err.message });
  }
});

// GET /api/finance/payment-invoices/:id - детали счета
app.get(`${API_PREFIX}/finance/payment-invoices/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query(
      `SELECT 
        id,
        invoice_number AS "invoiceNumber",
        created_by AS "createdBy",
        created_at AS "createdAt",
        updated_at AS "updatedAt",
        purpose_type AS "purposeType",
        purpose_building_ids AS "purposeBuildingIds",
        purpose_district_ids AS "purposeDistrictIds",
        purpose_description AS "purposeDescription",
        purpose_event_id AS "purposeEventId",
        plan_item_id AS "planItemId",
        plan_item_building_id AS "planItemBuildingId",
        payment_format AS "paymentFormat",
        item_type AS "itemType",
        contractor_name AS "contractorName",
        contractor_inn AS "contractorInn",
        service_description AS "serviceDescription",
        service_items AS "serviceItems",
        material_items AS "materialItems",
        total_amount AS "totalAmount",
        distribution_method AS "distributionMethod",
        distribution_data AS "distributionData",
        status,
        current_approver_role AS "currentApproverRole",
        approval_history AS "approvalHistory",
        rejection_reason AS "rejectionReason",
        scheduled_date AS "scheduledDate",
        payment_date AS "paymentDate",
        payment_ref AS "paymentRef",
        is_cash AS "isCash",
        postponed_date AS "postponedDate",
        cancel_reason AS "cancelReason",
        is_completed AS "isCompleted",
        closing_docs_received AS "closingDocsReceived",
        closing_docs_files AS "closingDocsFiles",
        notes,
        file_urls AS "fileUrls"
      FROM payment_invoices
      WHERE id = $1`,
      [id]
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Invoice not found' });
    }
    
    res.json(result[0]);
  } catch (err) {
    console.error('Error fetching payment invoice:', err);
    res.status(500).json({ error: 'Failed to fetch payment invoice', details: err.message });
  }
});

// POST /api/finance/payment-invoices - создание счета
app.post(`${API_PREFIX}/finance/payment-invoices`, async (req, res) => {
  try {
    const {
      createdBy,
      purposeType,
      purposeBuildingIds = [],
      purposeDistrictIds = [],
      purposeDescription,
      purposeEventId,
      planItemId,
      planItemBuildingId,
      paymentFormat,
      itemType = 'service',
      contractorName,
      contractorInn,
      serviceDescription, // Для обратной совместимости
      serviceItems, // Список услуг
      materialItems, // Список ТМЦ
      totalAmount,
      distributionMethod,
      distributionData = {},
      notes,
      fileUrls = []
    } = req.body;
    
    if (!createdBy || !purposeType || !paymentFormat || !contractorName || !totalAmount) {
      return res.status(400).json({ error: 'Missing required fields' });
    }

    // Проверка наличия услуг или ТМЦ в зависимости от типа
    if (itemType === 'service') {
      if (!serviceItems || serviceItems.length === 0) {
        return res.status(400).json({ error: 'Добавьте хотя бы одну услугу' });
      }
      // Проверяем, что все услуги заполнены
      for (let i = 0; i < serviceItems.length; i++) {
        const item = serviceItems[i];
        if (!item.name || !item.name.trim()) {
          return res.status(400).json({ error: `Услуга ${i + 1}: название обязательно` });
        }
        if (!item.amount || item.amount <= 0) {
          return res.status(400).json({ error: `Услуга ${i + 1}: сумма должна быть больше нуля` });
        }
      }
    } else if (itemType === 'materials') {
      if (!materialItems || materialItems.length === 0) {
        return res.status(400).json({ error: 'Добавьте хотя бы одну позицию ТМЦ' });
      }
      // Проверяем, что все ТМЦ заполнены
      for (let i = 0; i < materialItems.length; i++) {
        const item = materialItems[i];
        if (!item.name || !item.name.trim()) {
          return res.status(400).json({ error: `ТМЦ ${i + 1}: наименование обязательно` });
        }
        if (!item.quantity || item.quantity <= 0) {
          return res.status(400).json({ error: `ТМЦ ${i + 1}: количество должно быть больше нуля` });
        }
        if (!item.unit || !item.unit.trim()) {
          return res.status(400).json({ error: `ТМЦ ${i + 1}: единица измерения обязательна` });
        }
        if (!item.pricePerUnit || item.pricePerUnit <= 0) {
          return res.status(400).json({ error: `ТМЦ ${i + 1}: цена за единицу должна быть больше нуля` });
        }
      }
    }
    
    // Генерируем номер счета
    const invoiceNumber = paymentInvoiceWorkflow.generateInvoiceNumber();
    
    // Определяем начальный статус
    // Если создатель из высшего звена - пропускаем этап manager
    const userRoles = await getUserRoles(createdBy);
    const isTopManagement = userRoles.some(role => paymentInvoiceWorkflow.TOP_MANAGEMENT_ROLES.includes(role));
    const initialStatus = isTopManagement ? 'pending_finance_manager_approval' : 'pending_manager_approval';
    const currentApproverRole = isTopManagement ? 'finance_manager' : 'manager';
    // Формируем service_description из списков для обратной совместимости
    let finalServiceDescription = serviceDescription || '';
    if (!finalServiceDescription) {
      if (itemType === 'service' && serviceItems && serviceItems.length > 0) {
        finalServiceDescription = serviceItems.map((item) => `${item.name} - ${item.amount} ₽`).join('; ');
      } else if (itemType === 'materials' && materialItems && materialItems.length > 0) {
        finalServiceDescription = materialItems.map((item) => `${item.name} (${item.quantity} ${item.unit}) - ${item.amount} ₽`).join('; ');
      }
    }

    const result = await query(
      `INSERT INTO payment_invoices (
        invoice_number, created_by, purpose_type, purpose_building_ids, purpose_district_ids,
        purpose_description, purpose_event_id, plan_item_id, plan_item_building_id, payment_format, item_type, contractor_name, contractor_inn,
        service_description, service_items, material_items, total_amount, distribution_method, distribution_data,
        status, current_approver_role, notes, file_urls
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
      RETURNING id, invoice_number AS "invoiceNumber", created_at AS "createdAt"`,
      [
        invoiceNumber,
        createdBy,
        purposeType,
        JSON.stringify(purposeBuildingIds),
        JSON.stringify(purposeDistrictIds),
        purposeDescription || null,
        purposeEventId ? parseInt(purposeEventId, 10) : null,
        planItemId || null,
        planItemBuildingId || null,
        paymentFormat,
        itemType,
        contractorName,
        contractorInn || null,
        finalServiceDescription || null,
        itemType === 'service' ? JSON.stringify(serviceItems || []) : null,
        itemType === 'materials' ? JSON.stringify(materialItems || []) : null,
        totalAmount,
        distributionMethod || null,
        JSON.stringify(distributionData),
        initialStatus,
        currentApproverRole,
        notes || null,
        JSON.stringify(fileUrls)
      ]
    );

    try {
      const opts = {
        type: 'payment_invoice',
        title: 'Новый счёт на оплату',
        body: (contractorName || '') + (totalAmount ? ' — ' + totalAmount + ' ₽' : ''),
        entityType: 'payment_invoice',
        entityId: String(result[0].id),
      };
      const creatorUserId = createdBy != null ? Number(createdBy) : null;
      if (creatorUserId) {
        await notificationService.createNotification(pool, { ...opts, userId: creatorUserId });
      } else {
        await notificationService.createNotificationForResponsibleZone(pool, 'finance', 'invoices', opts);
      }
    } catch (notifErr) {
      console.warn('Notification (payment invoice create):', notifErr.message);
    }
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error creating payment invoice:', err);
    res.status(500).json({ error: 'Failed to create payment invoice', details: err.message });
  }
});

// POST /api/finance/payment-invoices/upload - загрузка файла для счета на оплату
app.post(`${API_PREFIX}/finance/payment-invoices/upload`, uploadInvoice.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'Файл не загружен' });
    }

    // Исправляем кодировку оригинального имени файла
    let originalName = req.file.originalname;
    try {
      const hasCyrillic = /[А-Яа-яЁё]/.test(originalName);
      const hasGarbled = /[ÐÑ]/i.test(originalName) || /[â€]/i.test(originalName);
      
      if (hasGarbled || (!hasCyrillic && /[^\x00-\x7F]/.test(originalName))) {
        try {
          const buffer = Buffer.from(originalName, 'latin1');
          const decoded = buffer.toString('utf8');
          if (/[А-Яа-яЁё]/.test(decoded)) {
            originalName = decoded;
          }
        } catch (decodeErr) {
          try {
            originalName = decodeURIComponent(escape(originalName));
          } catch (e) {
            // Используем оригинал
          }
        }
      }
    } catch (decodeErr) {
      console.warn('Ошибка декодирования имени файла:', decodeErr);
    }

    // Используем API endpoint для скачивания вместо прямого пути
    const fileUrl = `/api/finance/payment-invoices/download/${req.file.filename}`;
    const fileInfo = {
      filename: originalName,
      url: fileUrl,
      size: req.file.size,
      mimetype: req.file.mimetype,
      uploadedAt: new Date().toISOString(),
      storedFilename: req.file.filename
    };

    res.json({
      success: true,
      file: fileInfo,
      url: fileUrl,
      fileUrl: fileUrl
    });
  } catch (err) {
    console.error('Error uploading invoice file:', err);
    res.status(500).json({ error: 'Failed to upload file' });
  }
});

// GET /api/finance/payment-invoices/download/:filename - скачивание файла счета
app.get(`${API_PREFIX}/finance/payment-invoices/download/:filename`, (req, res) => {
  try {
    const filename = req.params.filename;
    const filePath = path.join(invoicesDir, filename);
    
    if (!fs.existsSync(filePath)) {
      return res.status(404).json({ error: 'Файл не найден' });
    }

    res.download(filePath, (err) => {
      if (err) {
        console.error('Error downloading file:', err);
        if (!res.headersSent) {
          res.status(500).json({ error: 'Ошибка при скачивании файла' });
        }
      }
    });
  } catch (err) {
    console.error('Error in download endpoint:', err);
    res.status(500).json({ error: 'Ошибка при скачивании файла' });
  }
});

// PUT /api/finance/payment-invoices/:id - обновление счета
app.put(`${API_PREFIX}/finance/payment-invoices/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const {
      purposeType,
      purposeBuildingIds,
      purposeDistrictIds,
      purposeDescription,
      purposeEventId,
      planItemId,
      planItemBuildingId,
      paymentFormat,
      itemType,
      contractorName,
      contractorInn,
      serviceDescription,
      serviceItems,
      materialItems,
      totalAmount,
      distributionMethod,
      distributionData,
      notes,
      fileUrls
    } = req.body;
    
    // Проверяем, что счет в статусе, который позволяет редактирование
    const currentInvoice = await query(
      'SELECT status FROM payment_invoices WHERE id = $1',
      [id]
    );
    
    if (currentInvoice.length === 0) {
      return res.status(404).json({ error: 'Invoice not found' });
    }
    
    if (!['draft', 'rejected'].includes(currentInvoice[0].status)) {
      return res.status(400).json({ error: 'Invoice cannot be edited in current status' });
    }
    
    const updateFields = [];
    const params = [];
    let paramIndex = 1;
    
    if (purposeType !== undefined) {
      updateFields.push(`purpose_type = $${paramIndex++}`);
      params.push(purposeType);
    }
    if (purposeBuildingIds !== undefined) {
      updateFields.push(`purpose_building_ids = $${paramIndex++}`);
      params.push(JSON.stringify(purposeBuildingIds));
    }
    if (purposeDistrictIds !== undefined) {
      updateFields.push(`purpose_district_ids = $${paramIndex++}`);
      params.push(JSON.stringify(purposeDistrictIds));
    }
    if (purposeDescription !== undefined) {
      updateFields.push(`purpose_description = $${paramIndex++}`);
      params.push(purposeDescription);
    }
    if (purposeEventId !== undefined) {
      updateFields.push(`purpose_event_id = $${paramIndex++}`);
      params.push(purposeEventId ? parseInt(purposeEventId, 10) : null);
    }
    if (planItemId !== undefined) {
      updateFields.push(`plan_item_id = $${paramIndex++}`);
      params.push(planItemId || null);
    }
    if (planItemBuildingId !== undefined) {
      updateFields.push(`plan_item_building_id = $${paramIndex++}`);
      params.push(planItemBuildingId || null);
    }
    if (paymentFormat !== undefined) {
      updateFields.push(`payment_format = $${paramIndex++}`);
      params.push(paymentFormat);
    }
    if (itemType !== undefined) {
      updateFields.push(`item_type = $${paramIndex++}`);
      params.push(itemType);
    }
    if (contractorName !== undefined) {
      updateFields.push(`contractor_name = $${paramIndex++}`);
      params.push(contractorName);
    }
    if (contractorInn !== undefined) {
      updateFields.push(`contractor_inn = $${paramIndex++}`);
      params.push(contractorInn);
    }
    if (serviceDescription !== undefined) {
      updateFields.push(`service_description = $${paramIndex++}`);
      params.push(serviceDescription);
    }
    if (serviceItems !== undefined) {
      updateFields.push(`service_items = $${paramIndex++}`);
      params.push(JSON.stringify(serviceItems));
    }
    if (materialItems !== undefined) {
      updateFields.push(`material_items = $${paramIndex++}`);
      params.push(JSON.stringify(materialItems));
    }
    if (totalAmount !== undefined) {
      updateFields.push(`total_amount = $${paramIndex++}`);
      params.push(totalAmount);
    }
    if (distributionMethod !== undefined) {
      updateFields.push(`distribution_method = $${paramIndex++}`);
      params.push(distributionMethod);
    }
    if (distributionData !== undefined) {
      updateFields.push(`distribution_data = $${paramIndex++}`);
      params.push(JSON.stringify(distributionData));
    }
    if (notes !== undefined) {
      updateFields.push(`notes = $${paramIndex++}`);
      params.push(notes);
    }
    if (fileUrls !== undefined) {
      updateFields.push(`file_urls = $${paramIndex++}`);
      params.push(JSON.stringify(fileUrls));
    }
    
    if (updateFields.length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }
    
    updateFields.push(`updated_at = NOW()`);
    params.push(id);
    
    const result = await query(
      `UPDATE payment_invoices 
       SET ${updateFields.join(', ')}
       WHERE id = $${paramIndex}
       RETURNING id, invoice_number AS "invoiceNumber", updated_at AS "updatedAt"`,
      params
    );
    
    res.json(result[0]);
  } catch (err) {
    console.error('Error updating payment invoice:', err);
    res.status(500).json({ error: 'Failed to update payment invoice', details: err.message });
  }
});

// POST /api/finance/payment-invoices/:id/approve - согласование счета
app.post(`${API_PREFIX}/finance/payment-invoices/:id/approve`, async (req, res) => {
  try {
    const { id } = req.params;
    const { userId, comment = '' } = req.body;
    
    if (!userId) {
      return res.status(400).json({ error: 'userId is required' });
    }
    
    // Получаем текущий счет
    const invoiceResult = await query(
      'SELECT status, approval_history, current_approver_role FROM payment_invoices WHERE id = $1',
      [id]
    );
    
    if (invoiceResult.length === 0) {
      return res.status(404).json({ error: 'Invoice not found' });
    }
    
    const currentInvoice = invoiceResult[0];
    const currentStatus = currentInvoice.status;
    
    // Получаем роли пользователя
    const userRoles = await getUserRoles(userId);
    
    // Проверяем, может ли пользователь согласовать
    if (!paymentInvoiceWorkflow.canApprove(currentStatus, userRoles)) {
      return res.status(403).json({ error: 'User does not have permission to approve this invoice' });
    }
    
    // Определяем следующий статус
    const nextStatus = paymentInvoiceWorkflow.getNextStatus(currentStatus, userRoles);
    if (!nextStatus) {
      return res.status(400).json({ error: 'Cannot determine next status' });
    }
    
    // Определяем роль следующего согласующего
    const nextApproverRole = paymentInvoiceWorkflow.getNextApproverRole(nextStatus, userRoles);
    
    // Создаем запись в истории
    const approvalHistory = currentInvoice.approval_history || [];
    const userRole = userRoles.find(r => 
      paymentInvoiceWorkflow.WORKFLOW_STAGES[currentStatus]?.role === r ||
      (paymentInvoiceWorkflow.WORKFLOW_STAGES[currentStatus]?.role === 'manager' && 
       paymentInvoiceWorkflow.TOP_MANAGEMENT_ROLES.includes(r))
    ) || userRoles[0] || 'unknown';
    
    approvalHistory.push(
      paymentInvoiceWorkflow.createApprovalHistoryEntry(userId, userRole, 'approve', comment)
    );
    
    // Обновляем счет
    const updateResult = await query(
      `UPDATE payment_invoices 
       SET status = $1, 
           current_approver_role = $2,
           approval_history = $3,
           updated_at = NOW()
       WHERE id = $4
       RETURNING id, status, current_approver_role AS "currentApproverRole", invoice_number AS "invoiceNumber"`,
      [nextStatus, nextApproverRole, JSON.stringify(approvalHistory), id]
    );
    const inv = updateResult[0];
    try {
      if (nextApproverRole) {
        const approverRows = await pool.query(
          'SELECT user_id FROM user_roles WHERE role = $1',
          [nextApproverRole]
        );
        const approverUserIds = approverRows.rows.map(r => r.user_id);
        await notificationService.createNotificationForUserIds(pool, approverUserIds, {
          type: 'invoice_approval',
          title: `Счёт на согласование`,
          body: `Счёт №${inv.invoiceNumber || id} ожидает вашего согласования`,
          entityType: 'payment_invoice',
          entityId: String(id),
        });
      } else {
        const createdByRows = await query('SELECT created_by FROM payment_invoices WHERE id = $1', [id]);
        const createdBy = createdByRows[0] && createdByRows[0].created_by;
        if (createdBy) {
          await notificationService.createNotification(pool, {
            userId: createdBy,
            type: 'invoice_approved',
            title: `Счёт согласован`,
            body: `Счёт №${inv.invoiceNumber || id} согласован`,
            entityType: 'payment_invoice',
            entityId: String(id),
          });
        }
      }
    } catch (notifErr) {
      console.warn('[notifications] invoice approve:', notifErr.message);
    }
    res.json(inv);
  } catch (err) {
    console.error('Error approving payment invoice:', err);
    res.status(500).json({ error: 'Failed to approve payment invoice', details: err.message });
  }
});

// POST /api/finance/payment-invoices/:id/reject - отклонение счета
app.post(`${API_PREFIX}/finance/payment-invoices/:id/reject`, async (req, res) => {
  try {
    const { id } = req.params;
    const { userId, reason } = req.body;
    
    if (!userId || !reason) {
      return res.status(400).json({ error: 'userId and reason are required' });
    }
    
    // Получаем текущий счет
    const invoiceResult = await query(
      'SELECT status, approval_history FROM payment_invoices WHERE id = $1',
      [id]
    );
    
    if (invoiceResult.length === 0) {
      return res.status(404).json({ error: 'Invoice not found' });
    }
    
    const currentInvoice = invoiceResult[0];
    const currentStatus = currentInvoice.status;
    
    // Получаем роли пользователя
    const userRoles = await getUserRoles(userId);
    
    // Проверяем, может ли пользователь отклонить
    if (!paymentInvoiceWorkflow.canApprove(currentStatus, userRoles)) {
      return res.status(403).json({ error: 'User does not have permission to reject this invoice' });
    }
    
    // Создаем запись в истории
    const approvalHistory = currentInvoice.approval_history || [];
    const userRole = userRoles.find(r => 
      paymentInvoiceWorkflow.WORKFLOW_STAGES[currentStatus]?.role === r ||
      (paymentInvoiceWorkflow.WORKFLOW_STAGES[currentStatus]?.role === 'manager' && 
       paymentInvoiceWorkflow.TOP_MANAGEMENT_ROLES.includes(r))
    ) || userRoles[0] || 'unknown';
    
    approvalHistory.push(
      paymentInvoiceWorkflow.createApprovalHistoryEntry(userId, userRole, 'reject', reason)
    );
    
    // Обновляем счет
    const updateResult = await query(
      `UPDATE payment_invoices 
       SET status = 'rejected',
           rejection_reason = $1,
           current_approver_role = NULL,
           approval_history = $2,
           updated_at = NOW()
       WHERE id = $3
       RETURNING id, status, created_by AS "createdBy", invoice_number AS "invoiceNumber"`,
      [reason, JSON.stringify(approvalHistory), id]
    );
    const inv = updateResult[0];
    try {
      if (inv.createdBy) {
        await notificationService.createNotification(pool, {
          userId: inv.createdBy,
          type: 'invoice_rejected',
          title: `Счёт отклонён`,
          body: `Счёт №${inv.invoiceNumber || id} отклонён. Причина: ${(reason || '').slice(0, 60)}${(reason || '').length > 60 ? '…' : ''}`,
          entityType: 'payment_invoice',
          entityId: String(id),
        });
      }
    } catch (notifErr) {
      console.warn('[notifications] invoice reject:', notifErr.message);
    }
    res.json({ id: inv.id, status: inv.status });
  } catch (err) {
    console.error('Error rejecting payment invoice:', err);
    res.status(500).json({ error: 'Failed to reject payment invoice', details: err.message });
  }
});

// POST /api/finance/payment-invoices/:id/schedule - постановка в график платежей
app.post(`${API_PREFIX}/finance/payment-invoices/:id/schedule`, async (req, res) => {
  try {
    const { id } = req.params;
    const { userId, scheduledDate } = req.body;
    
    if (!userId || !scheduledDate) {
      return res.status(400).json({ error: 'userId and scheduledDate are required' });
    }
    
    // Проверяем, что пользователь имеет право ставить в график (финансист или фин.рук)
    const userRoles = await getUserRoles(userId);
    if (!userRoles.includes('financier') && !userRoles.includes('finance_manager')) {
      return res.status(403).json({ error: 'Only finance_manager or financier can schedule invoices' });
    }
    
    // Проверяем, что счет в статусе, из которого можно ставить в график
    const invoiceResult = await query(
      'SELECT status FROM payment_invoices WHERE id = $1',
      [id]
    );
    
    if (invoiceResult.length === 0) {
      return res.status(404).json({ error: 'Invoice not found' });
    }
    
    const currentStatus = invoiceResult[0].status;
    const allowedStatuses = ['approved', 'completed'];
    
    if (!allowedStatuses.includes(currentStatus)) {
      return res.status(400).json({ 
        error: 'Invoice must be approved or completed before scheduling',
        currentStatus 
      });
    }
    
    // Получаем данные счета для записи в календарь
    const invoiceRow = await query(
      'SELECT contractor_name, service_description, total_amount FROM payment_invoices WHERE id = $1',
      [id]
    );
    const inv = invoiceRow[0];

    // Обновляем счет
    const updateResult = await query(
      `UPDATE payment_invoices 
       SET status = 'scheduled',
           scheduled_date = $1,
           updated_at = NOW()
       WHERE id = $2
       RETURNING id, status, scheduled_date AS "scheduledDate"`,
      [scheduledDate, id]
    );

    // Создаем запись в платежном календаре (расход по счету)
    try {
      await query(
        `INSERT INTO payment_calendar_entries (direction, type, payment_invoice_id, category, description, amount, scheduled_date, probability, currency, is_cash, contractor_name, created_by)
         VALUES ('outgoing', 'invoice', $1, '', $2, $3, $4, 'confirmed', 'RUB', false, $5, $6)`,
        [id, inv?.service_description || '', parseFloat(inv?.total_amount) || 0, scheduledDate, inv?.contractor_name || '', userId]
      );
    } catch (calErr) {
      console.warn('Payment calendar entry not created (table may not exist):', calErr.message);
    }

    res.json(updateResult[0]);
  } catch (err) {
    console.error('Error scheduling payment invoice:', err);
    res.status(500).json({ error: 'Failed to schedule payment invoice', details: err.message });
  }
});

// Вспомогательная функция: перенос ТМЦ из счета в склад дома/участка
async function transferInvoiceMaterialsToInventory(invoice) {
  try {
    const itemType = invoice.item_type;
    const purposeType = invoice.purpose_type;
    const materialItems = invoice.material_items || [];
    const purposeBuildingIds = Array.isArray(invoice.purpose_building_ids) ? invoice.purpose_building_ids : [];
    const purposeDistrictIds = Array.isArray(invoice.purpose_district_ids) ? invoice.purpose_district_ids : [];

    if (itemType !== 'materials' || !Array.isArray(materialItems) || materialItems.length === 0) {
      return;
    }

    const today = new Date().toISOString().split('T')[0];

    const mapMaterialToInventoryItem = (material) => {
      if (!material || !material.name) return null;

      const nameLower = String(material.name).toLowerCase();
      let category = 'material';

      if (
        nameLower.includes('инструмент') ||
        nameLower.includes('дрель') ||
        nameLower.includes('перфоратор') ||
        nameLower.includes('болгарка') ||
        nameLower.includes('отверт') ||
        nameLower.includes('ключ')
      ) {
        category = 'tool';
      } else if (
        nameLower.includes('дверь') ||
        nameLower.includes('окно') ||
        nameLower.includes('рама') ||
        nameLower.includes('блок')
      ) {
        category = 'door';
      } else if (
        nameLower.includes('расход') ||
        nameLower.includes('салфетк') ||
        nameLower.includes('перчатк') ||
        nameLower.includes('скотч')
      ) {
        category = 'consumable';
      }

      return {
        id: `inv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
        name: material.name,
        category,
        quantity: Number(material.quantity) || 0,
        unit: material.unit || 'шт.',
        lastCheck: today,
        source: 'invoice'
      };
    };

    const newItems = materialItems
      .map(mapMaterialToInventoryItem)
      .filter((item) => item && item.quantity > 0);

    if (newItems.length === 0) return;

    // Перенос на дом
    if (purposeType === 'building' && purposeBuildingIds.length > 0) {
      for (const buildingId of purposeBuildingIds) {
        const rows = await query('SELECT data FROM buildings WHERE id = $1', [buildingId]);
        if (!rows.length) continue;

        const building = rows[0].data || {};
        const existingInventory = Array.isArray(building.inventory) ? building.inventory : [];
        const updatedInventory = [...existingInventory];

        newItems.forEach((newItem) => {
          const existingIndex = updatedInventory.findIndex(
            (item) => item.name === newItem.name && item.unit === newItem.unit
          );

          if (existingIndex >= 0) {
            updatedInventory[existingIndex] = {
              ...updatedInventory[existingIndex],
              quantity: Number(updatedInventory[existingIndex].quantity || 0) + newItem.quantity
            };
          } else {
            updatedInventory.push(newItem);
          }
        });

        building.inventory = updatedInventory;
        await query('UPDATE buildings SET data = $2 WHERE id = $1', [buildingId, building]);
      }
    }

    // Перенос на участок
    if (purposeType === 'district' && purposeDistrictIds.length > 0) {
      for (const districtId of purposeDistrictIds) {
        const rows = await query(
          'SELECT COALESCE(inventory, \'[]\'::jsonb) AS inventory FROM districts WHERE id = $1',
          [districtId]
        );
        if (!rows.length) continue;

        const existingInventory = Array.isArray(rows[0].inventory) ? rows[0].inventory : [];
        const updatedInventory = [...existingInventory];

        newItems.forEach((newItem) => {
          const existingIndex = updatedInventory.findIndex(
            (item) => item.name === newItem.name && item.unit === newItem.unit
          );

          if (existingIndex >= 0) {
            updatedInventory[existingIndex] = {
              ...updatedInventory[existingIndex],
              quantity: Number(updatedInventory[existingIndex].quantity || 0) + newItem.quantity
            };
          } else {
            updatedInventory.push(newItem);
          }
        });

        await query('UPDATE districts SET inventory = $2 WHERE id = $1', [
          districtId,
          JSON.stringify(updatedInventory)
        ]);
      }
    }
  } catch (err) {
    console.error('[transferInvoiceMaterialsToInventory] Error:', err);
  }
}

// POST /api/finance/payment-invoices/:id/mark-completed - отметка "выполнено" / "получено" (для ТМЦ)
app.post(`${API_PREFIX}/finance/payment-invoices/:id/mark-completed`, async (req, res) => {
  try {
    const { id } = req.params;
    
    // Получаем счет
    const invoiceResult = await query(
      `SELECT 
        payment_format, 
        status,
        purpose_type,
        purpose_building_ids,
        purpose_district_ids,
        item_type,
        material_items
       FROM payment_invoices 
       WHERE id = $1`,
      [id]
    );
    
    if (invoiceResult.length === 0) {
      return res.status(404).json({ error: 'Invoice not found' });
    }
    
    const invoice = invoiceResult[0];
    
    // Для постоплаты: выполнено должно быть до оплаты
    // Для предоплаты/аванса: выполнено после оплаты
    if (invoice.payment_format === 'postpayment') {
      if (!['approved', 'scheduled'].includes(invoice.status)) {
        return res.status(400).json({ error: 'Postpayment invoice must be approved or scheduled before marking as completed' });
      }
    } else if (invoice.payment_format === 'prepayment' || invoice.payment_format === 'advance') {
      if (invoice.status !== 'paid') {
        return res.status(400).json({ error: 'Prepayment/advance invoice must be paid before marking as completed' });
      }
    }

    // Если это ТМЦ и счет привязан к дому/участку — переносим на склад
    await transferInvoiceMaterialsToInventory(invoice);
    
    // Обновляем счет
    const updateResult = await query(
      `UPDATE payment_invoices 
       SET is_completed = TRUE,
           status = CASE 
             WHEN payment_format = 'postpayment' THEN 'scheduled'
             ELSE status
           END,
           updated_at = NOW()
       WHERE id = $1
       RETURNING id, is_completed AS "isCompleted", status`,
      [id]
    );
    
    res.json(updateResult[0]);
  } catch (err) {
    console.error('Error marking invoice as completed:', err);
    res.status(500).json({ error: 'Failed to mark invoice as completed', details: err.message });
  }
});

// POST /api/finance/payment-invoices/:id/closing-docs - отметка получения закрывающих документов
app.post(`${API_PREFIX}/finance/payment-invoices/:id/closing-docs`, async (req, res) => {
  try {
    const { id } = req.params;
    const { received = true, files = [] } = req.body || {};

    // Получаем текущие файлы закрывающих документов
    const existingResult = await query(
      'SELECT closing_docs_files FROM payment_invoices WHERE id = $1',
      [id]
    );

    if (existingResult.length === 0) {
      return res.status(404).json({ error: 'Invoice not found' });
    }

    const currentFiles = existingResult[0].closing_docs_files || [];
    const newFiles = Array.isArray(files) ? files : [];
    const mergedFiles = [...currentFiles, ...newFiles];

    const updateResult = await query(
      `UPDATE payment_invoices
       SET closing_docs_received = $1,
           closing_docs_files = $2,
           updated_at = NOW()
       WHERE id = $3
       RETURNING id, closing_docs_received AS "closingDocsReceived", closing_docs_files AS "closingDocsFiles"`,
      [!!received, JSON.stringify(mergedFiles), id]
    );

    res.json(updateResult[0]);
  } catch (err) {
    console.error('Error updating closing docs flag:', err);
    res.status(500).json({ error: 'Failed to update closing docs flag', details: err.message });
  }
});

// POST /api/finance/payment-invoices/:id/update-payment-status - обновление статуса оплаты из календаря
app.post(`${API_PREFIX}/finance/payment-invoices/:id/update-payment-status`, async (req, res) => {
  try {
    const { id } = req.params;
    const { status, paymentDate, paymentRef, isCash, postponedDate, cancelReason } = req.body;
    
    if (!status || !['paid', 'postponed', 'cancelled'].includes(status)) {
      return res.status(400).json({ error: 'Invalid status. Must be paid, postponed, or cancelled' });
    }
    
    const updateFields = ['status = $1', 'updated_at = NOW()'];
    const params = [status];
    let idx = 2;
    
    if (status === 'paid') {
      if (paymentDate) {
        updateFields.push(`payment_date = $${idx++}`);
        params.push(paymentDate);
      }
      if (paymentRef !== undefined) {
        updateFields.push(`payment_ref = $${idx++}`);
        params.push(paymentRef || null);
      }
      if (isCash !== undefined) {
        updateFields.push(`is_cash = $${idx++}`);
        params.push(!!isCash);
      }
    } else if (status === 'postponed') {
      updateFields[0] = 'status = $1';
      params[0] = 'scheduled';
      if (postponedDate) {
        updateFields.push(`scheduled_date = $${idx++}`);
        updateFields.push(`postponed_date = $${idx++}`);
        params.push(postponedDate, postponedDate);
      }
    } else if (status === 'cancelled') {
      updateFields[0] = 'status = $1';
      params[0] = 'cancelled';
      if (cancelReason !== undefined) {
        updateFields.push(`cancel_reason = $${idx++}`);
        params.push(cancelReason || null);
      }
    }
    
    params.push(id);
    const updateResult = await query(
      `UPDATE payment_invoices 
       SET ${updateFields.join(', ')}
       WHERE id = $${params.length}
       RETURNING id, status, payment_date AS "paymentDate", payment_ref AS "paymentRef", is_cash AS "isCash", scheduled_date AS "scheduledDate", postponed_date AS "postponedDate", cancel_reason AS "cancelReason"`,
      params
    );
    
    res.json(updateResult[0]);
  } catch (err) {
    console.error('Error updating payment status:', err);
    res.status(500).json({ error: 'Failed to update payment status', details: err.message });
  }
});

// POST /api/finance/payment-invoices/:id/distribute - распределение суммы по домам/участкам
app.post(`${API_PREFIX}/finance/payment-invoices/:id/distribute`, async (req, res) => {
  try {
    const { id } = req.params;
    const { distributionMethod, distributionData } = req.body;
    
    if (!distributionMethod || !['equal', 'by_area', 'manual'].includes(distributionMethod)) {
      return res.status(400).json({ error: 'Invalid distribution method' });
    }
    
    if (!distributionData || typeof distributionData !== 'object') {
      return res.status(400).json({ error: 'distributionData is required' });
    }
    
    // Обновляем счет
    const updateResult = await query(
      `UPDATE payment_invoices 
       SET distribution_method = $1,
           distribution_data = $2,
           updated_at = NOW()
       WHERE id = $3
       RETURNING id, distribution_method AS "distributionMethod", distribution_data AS "distributionData"`,
      [distributionMethod, JSON.stringify(distributionData), id]
    );
    
    if (updateResult.length === 0) {
      return res.status(404).json({ error: 'Invoice not found' });
    }
    
    res.json(updateResult[0]);
  } catch (err) {
    console.error('Error distributing invoice:', err);
    res.status(500).json({ error: 'Failed to distribute invoice', details: err.message });
  }
});

// ========= РОЛИ ПОЛЬЗОВАТЕЛЕЙ =========

// GET /api/finance/user-roles - роли текущего пользователя
app.get(`${API_PREFIX}/finance/user-roles`, async (req, res) => {
  try {
    const { userId } = req.query;
    if (!userId) {
      return res.status(400).json({ error: 'userId is required' });
    }
    
    const result = await query(
      'SELECT id, user_id AS "userId", role, created_at AS "createdAt" FROM user_roles WHERE user_id = $1',
      [userId]
    );
    
    res.json(result);
  } catch (err) {
    console.error('Error fetching user roles:', err);
    res.status(500).json({ error: 'Failed to fetch user roles', details: err.message });
  }
});

// GET /api/finance/user-roles/:userId - роли конкретного пользователя
app.get(`${API_PREFIX}/finance/user-roles/:userId`, async (req, res) => {
  try {
    const { userId } = req.params;
    const result = await query(
      'SELECT id, user_id AS "userId", role, created_at AS "createdAt" FROM user_roles WHERE user_id = $1',
      [userId]
    );
    
    res.json(result);
  } catch (err) {
    console.error('Error fetching user roles:', err);
    res.status(500).json({ error: 'Failed to fetch user roles', details: err.message });
  }
});

// POST /api/finance/user-roles - назначение роли пользователю
app.post(`${API_PREFIX}/finance/user-roles`, async (req, res) => {
  try {
    const { userId, role } = req.body;
    
    if (!userId || !role) {
      return res.status(400).json({ error: 'userId and role are required' });
    }
    
    const validRoles = ['manager', 'finance_manager', 'financier', 'finance_director', 'director', 'top_management'];
    if (!validRoles.includes(role)) {
      return res.status(400).json({ error: 'Invalid role' });
    }
    
    const result = await query(
      `INSERT INTO user_roles (user_id, role)
       VALUES ($1, $2)
       ON CONFLICT (user_id, role) DO NOTHING
       RETURNING id, user_id AS "userId", role, created_at AS "createdAt"`,
      [userId, role]
    );
    
    if (result.length === 0) {
      return res.status(409).json({ error: 'Role already assigned' });
    }
    
    res.status(201).json(result[0]);
  } catch (err) {
    console.error('Error assigning user role:', err);
    res.status(500).json({ error: 'Failed to assign user role', details: err.message });
  }
});

// DELETE /api/finance/user-roles/:id - удаление роли
app.delete(`${API_PREFIX}/finance/user-roles/:id`, async (req, res) => {
  try {
    const { id } = req.params;
    const result = await query(
      'DELETE FROM user_roles WHERE id = $1 RETURNING id',
      [id]
    );
    
    if (result.length === 0) {
      return res.status(404).json({ error: 'Role not found' });
    }
    
    res.json({ success: true });
  } catch (err) {
    console.error('Error deleting user role:', err);
    res.status(500).json({ error: 'Failed to delete user role', details: err.message });
  }
});

// Функция для автоматического создания отчетов 1 числа каждого месяца
async function createMonthlyReportsForAllBuildings() {
  try {
    const now = new Date();
    const today = now.getDate();
    
    // Проверяем, что сегодня 1 число
    if (today !== 1) {
      return;
    }

    // Определяем период прошлого месяца
    const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
    const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59);
    
    const periodStart = lastMonth.toISOString().split('T')[0];
    const periodEnd = lastMonthEnd.toISOString().split('T')[0];
    
    const months = [
      'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
      'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
    ];
    const month = `${months[lastMonth.getMonth()]} ${lastMonth.getFullYear()}`;

    // Получаем все дома
    const buildingsResult = await query('SELECT id FROM buildings');
    const buildingIds = buildingsResult.map(row => row.id);

    if (buildingIds.length === 0) {
      console.log('[Auto Reports] Нет домов для создания отчетов');
      return;
    }

    console.log(`[Auto Reports] Начинаем создание отчетов за ${month} для ${buildingIds.length} домов...`);

    let createdCount = 0;
    let updatedCount = 0;

    // Создаем отчеты для каждого дома
    for (const buildingId of buildingIds) {
      try {
        // Проверяем, существует ли уже отчет за этот период
        const existing = await query(
          `SELECT id FROM resident_reports 
           WHERE building_id = $1 AND period_start = $2 AND period_end = $3`,
          [buildingId, periodStart, periodEnd]
        );

        if (existing.length > 0) {
          // Обновляем существующий отчет (если он в статусе черновик)
          const reportId = existing[0].id;
          await query(
            `UPDATE resident_reports 
             SET month = $1, updated_at = NOW()
             WHERE id = $2 AND status = 'draft'`,
            [month, reportId]
          );
          updatedCount++;
        } else {
          // Создаем новый отчет
          await query(
            `INSERT INTO resident_reports 
             (building_id, month, period_start, period_end, status, content)
             VALUES ($1, $2, $3, $4, 'draft', '{}'::jsonb)`,
            [buildingId, month, periodStart, periodEnd]
          );
          createdCount++;
        }
      } catch (err) {
        console.error(`[Auto Reports] Ошибка создания отчета для дома ${buildingId}:`, err.message);
        // Продолжаем создание отчетов для других домов
      }
    }

    console.log(`[Auto Reports] Завершено: создано ${createdCount}, обновлено ${updatedCount} отчетов за ${month}`);
  } catch (err) {
    console.error('[Auto Reports] Ошибка автоматического создания отчетов:', err);
  }
}

// Функция для автоматического создания опросов NPS для всех домов
// Создает один опрос для каждого дома один раз (не пересоздается каждый месяц)
async function createNPSSurveysForAllBuildings() {
  try {
    // Получаем все дома
    const buildingsResult = await query('SELECT id FROM buildings');
    const buildingIds = buildingsResult.map(row => row.id);

    if (buildingIds.length === 0) {
      console.log('[Auto NPS] Нет домов для создания опросов');
      return;
    }

    console.log(`[Auto NPS] Проверка опросов для ${buildingIds.length} домов...`);

    let createdCount = 0;

    // Создаем опросы для каждого дома, если их еще нет
    for (const buildingId of buildingIds) {
      try {
        // Проверяем, существует ли уже опрос для этого дома (любой статус)
        const existing = await query(
          `SELECT id FROM nps_surveys 
           WHERE building_id = $1`,
          [buildingId]
        );

        if (existing.length === 0) {
          // Создаем новый опрос только если его еще нет
          const accessKey = `nps-${buildingId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
          
          await query(
            `INSERT INTO nps_surveys 
             (building_id, title, description, status, access_key, created_by)
             VALUES ($1, $2, $3, $4, $5, 'system')`,
            [
              buildingId,
              'Опрос удовлетворенности жителей',
              'Помогите нам стать лучше! Поделитесь своим мнением о качестве обслуживания.',
              'draft',
              accessKey
            ]
          );
          createdCount++;
        }
      } catch (err) {
        console.error(`[Auto NPS] Ошибка создания опроса для дома ${buildingId}:`, err.message);
        // Продолжаем создание опросов для других домов
      }
    }

    if (createdCount > 0) {
      console.log(`[Auto NPS] Создано ${createdCount} новых опросов`);
    }
  } catch (err) {
    console.error('[Auto NPS] Ошибка автоматического создания опросов:', err);
  }
}

// Проверка при запуске сервера и ежедневная проверка
async function checkAndCreateMonthlyReports() {
  try {
    await createMonthlyReportsForAllBuildings();
  } catch (err) {
    console.error('[Auto Reports] Ошибка проверки создания отчетов:', err);
  }
}

// Проверка и создание опросов NPS при запуске сервера
async function checkAndCreateNPSSurveys() {
  try {
    await createNPSSurveysForAllBuildings();
  } catch (err) {
    console.error('[Auto NPS] Ошибка проверки создания опросов:', err);
  }
}

// Запускаем проверку при старте сервера
if (DATABASE_URL) {
  // Небольшая задержка, чтобы БД успела инициализироваться
  setTimeout(() => {
    checkAndCreateMonthlyReports();
    checkAndCreateNPSSurveys(); // Создаем опросы NPS при старте
  }, 5000);
}

// Проверяем каждый день в 2:00 ночи
setInterval(() => {
  checkAndCreateMonthlyReports();
}, 24 * 60 * 60 * 1000); // 24 часа

// POST /api/pr/reports/auto-create - ручной запуск создания отчетов
app.post(`${API_PREFIX}/pr/reports/auto-create`, async (req, res) => {
  try {
    await createMonthlyReportsForAllBuildings();
    await createNPSSurveysForAllBuildings();
    res.json({ success: true, message: 'Проверка создания отчетов и опросов выполнена' });
  } catch (err) {
    console.error('Error in auto-create reports:', err);
    res.status(500).json({ error: 'Failed to create reports', details: err.message });
  }
});

startServer();


