// 客户经营 CRM · 云端联网版主进程
// 本文件只做三件事：窗口管理、本地文件对话框、把渲染层的 IPC 调用转成服务器 HTTP 请求。
// 所有业务规则、权限校验、数据存储都在服务器端，客户端不再保存任何业务数据。
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
const crypto = require('crypto');
const { URL, URLSearchParams } = require('url');

const APP_VERSION = '2.0.3';
// 默认服务器地址；员工也可以在登录页自行修改，修改后写入本机配置文件。
const DEFAULT_SERVER_URL = 'http://crm.stxxjituan.com';
const UPLOAD_LIMIT = 80 * 1024 * 1024;

let configPath = '';
let tempDir = '';
let serverUrl = DEFAULT_SERVER_URL;
let authToken = null;
let currentUserInfo = null;

// ---------- 服务器地址配置 ----------
function normalizeServerUrl(value) {
  let s = String(value || '').trim();
  if (!s) return DEFAULT_SERVER_URL;
  if (!/^https?:\/\//i.test(s)) s = 'http://' + s;
  return s.replace(/\/+$/, '');
}
function loadConfig() {
  try {
    if (fs.existsSync(configPath)) {
      const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
      serverUrl = normalizeServerUrl(cfg.serverUrl);
      return;
    }
  } catch (e) {
    console.warn('读取服务器配置失败，使用默认地址：', e.message);
  }
  serverUrl = DEFAULT_SERVER_URL;
  saveConfig();
}
function saveConfig() {
  try {
    fs.writeFileSync(configPath, JSON.stringify({
      serverUrl,
      说明: '这是本机连接的 CRM 服务器地址。也可以在软件登录页面直接修改。'
    }, null, 2), 'utf8');
  } catch (e) {
    console.warn('保存服务器配置失败：', e.message);
  }
}

// ---------- HTTP 基础层 ----------
function friendlyNetError(err) {
  const code = err && err.code;
  if (code === 'ECONNREFUSED') return new Error(`无法连接服务器（${serverUrl}），请确认服务器已启动、地址填写正确。`);
  if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') return new Error(`找不到服务器地址（${serverUrl}），请检查登录页的服务器地址。`);
  if (code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'EPIPE') return new Error('与服务器通信中断，请检查网络后重试。');
  return new Error((err && err.message) || '网络请求失败，请稍后重试。');
}
function parseDownloadName(headers) {
  const cd = String((headers || {})['content-disposition'] || '');
  const star = cd.match(/filename\*=UTF-8''([^;]+)/i);
  if (star) { try { return decodeURIComponent(star[1].trim().replace(/^"|"$/g, '')); } catch (e) { /* 继续尝试普通字段 */ } }
  const plain = cd.match(/filename="?([^";]+)"?/i);
  return plain ? plain[1] : '';
}
// options: { url, method, headers, timeout, saveTo }
function request(options, bodyBuffer) {
  return new Promise((resolve, reject) => {
    let target;
    try { target = new URL(options.url); } catch (e) { return reject(new Error('服务器地址无效：' + options.url)); }
    const mod = target.protocol === 'https:' ? https : http;
    const req = mod.request({
      protocol: target.protocol,
      hostname: target.hostname,
      port: target.port || (target.protocol === 'https:' ? 443 : 80),
      path: target.pathname + target.search,
      method: options.method || 'GET',
      headers: options.headers || {}
    }, res => {
      if (options.saveTo && res.statusCode < 400) {
        const out = fs.createWriteStream(options.saveTo);
        res.pipe(out);
        out.on('finish', () => out.close(() => resolve({ statusCode: res.statusCode, headers: res.headers, saved: options.saveTo })));
        out.on('error', e => reject(e));
        return;
      }
      const chunks = [];
      res.on('data', c => chunks.push(c));
      res.on('end', () => resolve({ statusCode: res.statusCode, headers: res.headers, text: Buffer.concat(chunks).toString('utf8') }));
    });
    req.setTimeout(options.timeout || 30000, () => req.destroy(Object.assign(new Error('请求超时'), { code: 'ETIMEDOUT' })));
    req.on('error', e => reject(friendlyNetError(e)));
    if (bodyBuffer) req.write(bodyBuffer);
    req.end();
  });
}
function authHeaders(extra) {
  const h = Object.assign({ 'Accept': 'application/json' }, extra || {});
  if (authToken) h['Authorization'] = 'Bearer ' + authToken;
  return h;
}
function unwrap(res) {
  let data;
  try { data = JSON.parse(res.text); } catch (e) {
    if (res.statusCode >= 500) throw new Error(`服务器内部错误（HTTP ${res.statusCode}），请联系管理员。`);
    throw new Error(`服务器返回异常（HTTP ${res.statusCode}）。`);
  }
  if (res.statusCode === 401) { authToken = null; currentUserInfo = null; throw new Error(data.message || '登录状态已过期，请退出后重新登录。'); }
  if (res.statusCode >= 400 || data.ok === false) throw new Error(data.message || '操作失败。');
  return data;
}
function requireLogin() {
  if (!authToken) throw new Error('登录状态无效，请重新登录。');
}
function toQuery(query) {
  if (!query) return '';
  const pairs = Object.entries(query)
    .filter(([, v]) => v !== undefined && v !== null && v !== '')
    .map(([k, v]) => [k, String(v)]);
  if (!pairs.length) return '';
  return '?' + new URLSearchParams(pairs).toString();
}
async function apiGet(pathname, query) {
  requireLogin();
  return unwrap(await request({ url: serverUrl + pathname + toQuery(query), method: 'GET', headers: authHeaders() }));
}
async function apiPost(pathname, body) {
  requireLogin();
  const buf = Buffer.from(JSON.stringify(body || {}), 'utf8');
  return unwrap(await request({
    url: serverUrl + pathname,
    method: 'POST',
    headers: authHeaders({ 'Content-Type': 'application/json', 'Content-Length': buf.length })
  }, buf));
}
async function apiUpload(pathname, filePath, fields) {
  requireLogin();
  if (!fs.existsSync(filePath)) throw new Error('选择的文件不存在，请重新选择。');
  const stat = fs.statSync(filePath);
  if (stat.size > UPLOAD_LIMIT) throw new Error(`文件大小 ${(stat.size / 1024 / 1024).toFixed(1)} MB，超出 80 MB 上限。`);
  const boundary = '----CRMFormBoundary' + crypto.randomBytes(12).toString('hex');
  const originalName = path.basename(filePath);
  const parts = [];
  const allFields = Object.assign({}, fields || {}, { originalName });
  for (const [k, v] of Object.entries(allFields)) {
    if (v === undefined || v === null || v === '') continue;
    parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${k}"\r\n\r\n${String(v)}\r\n`, 'utf8'));
  }
  parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${originalName}"\r\nContent-Type: application/octet-stream\r\n\r\n`, 'utf8'));
  parts.push(fs.readFileSync(filePath));
  parts.push(Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8'));
  const body = Buffer.concat(parts);
  return unwrap(await request({
    url: serverUrl + pathname,
    method: 'POST',
    timeout: 180000,
    headers: authHeaders({ 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': body.length })
  }, body));
}
async function apiDownload(pathname, destPath) {
  requireLogin();
  const res = await request({ url: serverUrl + pathname, method: 'GET', timeout: 180000, headers: authHeaders(), saveTo: destPath });
  if (res.statusCode >= 400) {
    try { fs.unlinkSync(destPath); } catch (e) { /* 忽略清理失败 */ }
    let message = '下载失败，请稍后重试。';
    try { message = JSON.parse(res.text).message || message; } catch (e) { /* 保持默认提示 */ }
    if (res.statusCode === 401) { authToken = null; currentUserInfo = null; }
    throw new Error(message);
  }
  return { savedPath: destPath, downloadName: parseDownloadName(res.headers) };
}

// ---------- 窗口 ----------
function createWindow() {
  const win = new BrowserWindow({
    width: 1380, height: 820, minWidth: 1120, minHeight: 680, show: false, backgroundColor: '#f5f7fa',
    webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, backgroundThrottling: false }
  });
  win.setMenuBarVisibility(false);
  win.once('ready-to-show', () => { win.show(); win.focus(); });
  win.webContents.on('did-finish-load', () => win.webContents.focus());
  win.loadFile(path.join(__dirname, 'index.html'));
}
app.whenReady().then(() => {
  configPath = path.join(app.getPath('userData'), 'server-config.json');
  tempDir = path.join(app.getPath('temp'), 'crm-cloud-cache');
  fs.mkdirSync(tempDir, { recursive: true });
  loadConfig();
  createWindow();
  app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
});
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });

// ---------- 服务器地址 / 登录 ----------
ipcMain.handle('app:getServer', () => ({ serverUrl, configPath, defaultServerUrl: DEFAULT_SERVER_URL }));
ipcMain.handle('app:setServer', (_, value) => {
  const next = normalizeServerUrl(value);
  if (!/^https?:\/\/[^\s/]+$/i.test(next)) throw new Error('服务器地址格式不正确，示例：crm.stxxjituan.com 或 http://crm.gongsi.com');
  serverUrl = next;
  authToken = null;
  currentUserInfo = null;
  saveConfig();
  return { serverUrl };
});
ipcMain.handle('app:ping', async () => {
  const res = await request({ url: serverUrl + '/api/app/info', method: 'GET', timeout: 8000, headers: { 'Accept': 'application/json' } });
  if (res.statusCode >= 400) throw new Error(`服务器无响应（HTTP ${res.statusCode}）。`);
  let data = {};
  try { data = JSON.parse(res.text); } catch (e) { throw new Error('该地址不是有效的 CRM 服务器。'); }
  return { ok: true, serverUrl, serverVersion: data.version || '' };
});
ipcMain.handle('login', async (_, input) => {
  const payload = { username: String((input || {}).username || '').trim(), password: String((input || {}).password || '') };
  const buf = Buffer.from(JSON.stringify(payload), 'utf8');
  const res = await request({
    url: serverUrl + '/api/login', method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Content-Length': buf.length, 'Accept': 'application/json' }
  }, buf);
  let data;
  try { data = JSON.parse(res.text); } catch (e) { throw new Error(`服务器返回异常（HTTP ${res.statusCode}），请确认服务器地址是否正确。`); }
  if (res.statusCode >= 400) throw new Error(data.message || '登录失败。');
  if (!data.ok) return { ok: false, message: data.message || '账号、密码错误或账号已停用。' };
  authToken = data.token;
  currentUserInfo = data.user;
  return { ok: true, user: data.user };
});
ipcMain.handle('logout', () => { authToken = null; currentUserInfo = null; return { ok: true }; });

// ---------- 启动数据 ----------
ipcMain.handle('bootstrap', async () => {
  const d = await apiGet('/api/bootstrap');
  return {
    companies: d.companies || [], users: d.users || [], customers: d.customers || [],
    leads: d.leads || [], contracts: d.contracts || [], followups: d.followups || [], attachments: d.attachments || []
  };
});

// ---------- 分公司 / 操作日志 ----------
ipcMain.handle('companies:list', async () => (await apiGet('/api/companies')).companies || []);
ipcMain.handle('audit:list', async () => (await apiGet('/api/audit')).audit || []);
ipcMain.handle('companies:add', async (_, u, input) => (await apiPost('/api/companies/add', { name: (input || {}).name })).company);
ipcMain.handle('companies:update', async (_, u, input) => (await apiPost('/api/companies/update', { id: (input || {}).id, name: (input || {}).name })).company);
ipcMain.handle('companies:delete', async (_, u, companyId) => apiPost('/api/companies/delete', { companyId }));

// ---------- 账号与员工 ----------
ipcMain.handle('users:list', async () => (await apiGet('/api/users')).users || []);
ipcMain.handle('employees:stats', async () => (await apiGet('/api/employees/stats')).employees || []);
ipcMain.handle('users:add', async (_, u, input) => (await apiPost('/api/users/add', input || {})).user);
ipcMain.handle('users:update', async (_, u, input) => (await apiPost('/api/users/update', input || {})).user);
ipcMain.handle('users:resetPassword', async (_, u, input) => apiPost('/api/users/resetPassword', input || {}));
ipcMain.handle('users:transfer', async (_, u, input) => apiPost('/api/users/transfer', input || {}));

// ---------- 客户 / 公海 ----------
ipcMain.handle('customers:list', async (_, u, filters = {}) => (await apiGet('/api/customers', {
  keyword: filters.keyword, source: filters.source, status: filters.status, ownerId: filters.ownerId
})).customers || []);
ipcMain.handle('customers:add', async (_, u, input) => (await apiPost('/api/customers/add', input || {})).customer);
ipcMain.handle('customers:update', async (_, u, input) => (await apiPost('/api/customers/update', input || {})).customer);
ipcMain.handle('customers:delete', async (_, u, customerId) => apiPost('/api/customers/delete', { customerId }));
ipcMain.handle('customers:toPublic', async (_, u, customerId) => apiPost('/api/customers/toPublic', { customerId }));
ipcMain.handle('public:list', async () => (await apiGet('/api/public')).customers || []);
ipcMain.handle('public:claim', async (_, u, customerId) => (await apiPost('/api/public/claim', { customerId })).customer);

// ---------- 线索池 ----------
ipcMain.handle('leads:list', async (_, u, filters = {}) => {
  const d = await apiGet('/api/leads', { page: filters.page, pageSize: filters.pageSize });
  return { leads: d.leads || [], pagination: d.pagination || { page: 1, pageSize: 20, total: 0, totalPages: 1 } };
});
ipcMain.handle('leads:add', async (_, u, input) => apiPost('/api/leads/add', input || {}));
ipcMain.handle('leads:import', async () => {
  const r = await dialog.showOpenDialog({ title: '导入线索 Excel', properties: ['openFile'], filters: [{ name: 'Excel 文件', extensions: ['xlsx'] }] });
  if (r.canceled || !r.filePaths[0]) return { cancelled: true };
  const d = await apiUpload('/api/leads/import', r.filePaths[0], {});
  if (d.cancelled) return { cancelled: true };
  const failedRows = Array.isArray(d.failedRows) ? d.failedRows : [];
  let errorFilePath = '';
  if (failedRows.length) errorFilePath = await saveLeadImportErrorsToDesktop(failedRows);
  return { imported: Number(d.imported || 0), duplicated: Number(d.duplicated || 0), failed: Number(d.failed || failedRows.length), errorFilePath };
});
ipcMain.handle('leads:assign', async (_, u, input) => (await apiPost('/api/leads/assign', input || {})).customer);
ipcMain.handle('leads:claim', async (_, u, leadId) => (await apiPost('/api/leads/claim', { id: leadId })).customer);
ipcMain.handle('leads:deleteBatch', async (_, u, ids) => apiPost('/api/leads/deleteBatch', { ids: Array.isArray(ids) ? ids : [] }));
ipcMain.handle('leads:convert', async (_, u, leadId) => (await apiPost('/api/leads/convert', { leadId })).customer);
async function saveLeadImportErrorsToDesktop(failedRows) {
  const desktop = app.getPath('desktop');
  const filePath = path.join(desktop, `线索导入失败明细-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.xlsx`);
  const headers = ['企业名称', '手机', '法定代表人', '客户来源', '主营业务', '企业状态', '行业分类', '注册地址', '所属分公司ID', '原始行号', '错误原因'];
  const rows = failedRows.map(item => {
    const raw = item && item.data && typeof item.data === 'object' ? item.data : {};
    return headers.map(h => h === '原始行号' ? (item.rowNo || '') : h === '错误原因' ? String(item.reason || '未知错误') : String(raw[h] || ''));
  });
  writeSimpleXlsx(filePath, '未导入线索', [headers, ...rows]);
  return filePath;
}
function xmlEscape(value) { return String(value ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;').replace(/'/g, '&apos;'); }
function colName(index) { let out = ''; for (let n = index + 1; n; n = Math.floor((n - 1) / 26)) out = String.fromCharCode(65 + (n - 1) % 26) + out; return out; }
function crc32(buffer) { let crc = -1; for (const b of buffer) { crc ^= b; for (let i = 0; i < 8; i++) crc = (crc >>> 1) ^ (0xEDB88320 & -(crc & 1)); } return (crc ^ -1) >>> 0; }
function zipStore(entries) {
  const parts = [], central = []; let offset = 0;
  for (const [name, body] of entries) {
    const data = Buffer.isBuffer(body) ? body : Buffer.from(body, 'utf8'), file = Buffer.from(name, 'utf8'), crc = crc32(data);
    const head = Buffer.alloc(30); head.writeUInt32LE(0x04034b50, 0); head.writeUInt16LE(20, 4); head.writeUInt16LE(0x0800, 6); head.writeUInt16LE(0, 8); head.writeUInt32LE(crc, 14); head.writeUInt32LE(data.length, 18); head.writeUInt32LE(data.length, 22); head.writeUInt16LE(file.length, 26); head.writeUInt16LE(0, 28);
    parts.push(head, file, data);
    const cd = Buffer.alloc(46); cd.writeUInt32LE(0x02014b50, 0); cd.writeUInt16LE(20, 4); cd.writeUInt16LE(20, 6); cd.writeUInt16LE(0x0800, 8); cd.writeUInt16LE(0, 10); cd.writeUInt32LE(crc, 16); cd.writeUInt32LE(data.length, 20); cd.writeUInt32LE(data.length, 24); cd.writeUInt16LE(file.length, 28); cd.writeUInt16LE(0, 30); cd.writeUInt16LE(0, 32); cd.writeUInt16LE(0, 34); cd.writeUInt32LE(0, 38); cd.writeUInt32LE(offset, 42);
    central.push(cd, file); offset += head.length + file.length + data.length;
  }
  const centralBuf = Buffer.concat(central), end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(entries.length, 8); end.writeUInt16LE(entries.length, 10); end.writeUInt32LE(centralBuf.length, 12); end.writeUInt32LE(offset, 16);
  return Buffer.concat([...parts, centralBuf, end]);
}
function writeSimpleXlsx(filePath, sheetName, table) {
  const widths = table[0].map((h, i) => Math.min(48, Math.max(String(h).length + 2, ...table.slice(1).map(r => String(r[i] || '').length + 2))));
  const cells = table.map((row, r) => `<row r="${r + 1}">${row.map((v, c) => `<c r="${colName(c)}${r + 1}" t="inlineStr"><is><t${/^\s|\s$/.test(String(v)) ? ' xml:space="preserve"' : ''}>${xmlEscape(v)}</t></is></c>`).join('')}</row>`).join('');
  const sheet = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews><cols>${widths.map((w, i) => `<col min="${i + 1}" max="${i + 1}" width="${w}" customWidth="1"/>`).join('')}</cols><sheetData>${cells}</sheetData></worksheet>`;
  const entries = [
    ['[Content_Types].xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>'],
    ['_rels/.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>'],
    ['xl/workbook.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="${xmlEscape(sheetName)}" sheetId="1" r:id="rId1"/></sheets></workbook>`],
    ['xl/_rels/workbook.xml.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/></Relationships>'],
    ['xl/worksheets/sheet1.xml', sheet]
  ];
  fs.writeFileSync(filePath, zipStore(entries));
}
if (process.env.CRM_TEST_EXPORTS === '1') module.exports = { saveLeadImportErrorsToDesktop, writeSimpleXlsx };

// ---------- 跟进记录 ----------
ipcMain.handle('followups:list', async (_, u, customerId) => (await apiGet('/api/followups', { customerId })).followups || []);
ipcMain.handle('followups:add', async (_, u, input) => (await apiPost('/api/followups/add', input || {})).followup);

// ---------- 合同 ----------
ipcMain.handle('contracts:list', async (_, u, filters = {}) => (await apiGet('/api/contracts', {
  keyword: filters.keyword, status: filters.status
})).contracts || []);
ipcMain.handle('contracts:add', async (_, u, input) => (await apiPost('/api/contracts/add', input || {})).contract);
ipcMain.handle('contracts:updateServices', async (_, u, input) => (await apiPost('/api/contracts/updateServices', input || {})).contract);
ipcMain.handle('contracts:update', async (_, u, input) => (await apiPost('/api/contracts/update', input || {})).contract);
ipcMain.handle('contracts:delete', async (_, u, contractId) => {
  const d = await apiPost('/api/contracts/delete', { contractId });
  return { id: d.id, removedAttachments: Number(d.removedAttachments || 0) };
});

// ---------- 合同附件 ----------
ipcMain.handle('attachments:list', async (_, u, contractId) => (await apiGet('/api/attachments', { contractId })).attachments || []);
ipcMain.handle('attachments:add', async (_, u, contractId) => {
  const r = await dialog.showOpenDialog({ title: '选择合同附件', properties: ['openFile'], filters: [{ name: '合同文件', extensions: ['pdf', 'jpg', 'jpeg', 'png'] }] });
  if (r.canceled || !r.filePaths[0]) return null;
  return (await apiUpload('/api/attachments/add', r.filePaths[0], { contractId })).attachment;
});
ipcMain.handle('attachments:open', async (_, u, attachmentId) => {
  const id = Number(attachmentId);
  if (!Number.isFinite(id)) throw new Error('附件编号无效。');
  const tmpName = `att-${id}-${Date.now()}.download`;
  const tmpPath = path.join(tempDir, tmpName);
  const { downloadName } = await apiDownload(`/api/attachments/${id}/download`, tmpPath);
  const finalName = downloadName || `附件-${id}`;
  const finalPath = path.join(tempDir, `${id}-${finalName}`);
  try { if (fs.existsSync(finalPath)) fs.unlinkSync(finalPath); } catch (e) { /* 覆盖旧缓存失败可忽略 */ }
  fs.renameSync(tmpPath, finalPath);
  const openError = await shell.openPath(finalPath);
  if (openError) throw new Error('附件已下载到本机缓存，但系统未能打开：' + openError);
  return { ok: true, cachedPath: finalPath };
});

// ---------- 学习资料 ----------
ipcMain.handle('learning:list', async () => (await apiGet('/api/learning')).materials || []);
ipcMain.handle('learning:pick', async () => {
  const r = await dialog.showOpenDialog({ title: '选择学习资料', properties: ['openFile'], filters: [{ name: '学习资料', extensions: ['pdf', 'doc', 'docx', 'pptx'] }] });
  if (r.canceled || !r.filePaths[0]) return null;
  const source = r.filePaths[0];
  const ext = path.extname(source).toLowerCase();
  if (!['.pdf', '.doc', '.docx', '.pptx'].includes(ext)) throw new Error('仅支持 PDF、DOC、DOCX、PPTX 文件。');
  const stat = fs.statSync(source);
  if (stat.size > UPLOAD_LIMIT) throw new Error(`文件大小 ${(stat.size / 1024 / 1024).toFixed(1)} MB，超出 80 MB 上限。`);
  return { path: source, name: path.basename(source), fileType: ext.slice(1) };
});
ipcMain.handle('learning:add', async (_, u, input = {}) => {
  let filePath = input.filePath;
  if (!filePath) {
    const r = await dialog.showOpenDialog({ title: '选择学习资料', properties: ['openFile'], filters: [{ name: '学习资料', extensions: ['pdf', 'doc', 'docx', 'pptx'] }] });
    if (r.canceled || !r.filePaths[0]) return null;
    filePath = r.filePaths[0];
  }
  const ext = path.extname(String(filePath)).toLowerCase();
  if (!['.pdf', '.doc', '.docx', '.pptx'].includes(ext)) throw new Error('仅支持 PDF、DOC、DOCX、PPTX 文件。');
  return (await apiUpload('/api/learning/add', String(filePath), {
    title: input.title, note: input.note, scope: input.scope, companyId: input.companyId
  })).material;
});
ipcMain.handle('learning:update', async (_, u, input = {}) => (await apiPost('/api/learning/update', input)).material);
ipcMain.handle('learning:delete', async (_, u, id) => apiPost('/api/learning/delete', { id }));
ipcMain.handle('learning:download', async (_, u, id) => {
  const materials = (await apiGet('/api/learning')).materials || [];
  const row = materials.find(x => Number(x.id) === Number(id));
  if (!row) throw new Error('学习资料不存在或无权下载。');
  const r = await dialog.showSaveDialog({ title: '保存学习资料', defaultPath: row.originalName || `学习资料-${id}` });
  if (r.canceled || !r.filePath) return { cancelled: true };
  await apiDownload(`/api/learning/${Number(id)}/download`, r.filePath);
  return { ok: true, filePath: r.filePath };
});

// ---------- 版本与更新 ----------
ipcMain.handle('app:updateCheck', async () => {
  const res = await request({ url: serverUrl + '/version.json', method: 'GET', timeout: 12000, headers: { 'Accept': 'application/json' } });
  if (res.statusCode === 404) return { configured: false, currentVersion: APP_VERSION };
  if (res.statusCode >= 400) throw new Error(`获取版本信息失败（HTTP ${res.statusCode}）。`);
  let manifest;
  try { manifest = JSON.parse(res.text); } catch (e) { throw new Error('服务器版本信息格式异常，请联系管理员。'); }
  return {
    configured: true,
    currentVersion: APP_VERSION,
    latestVersion: String(manifest.version || ''),
    notes: String(manifest.notes || ''),
    downloadUrl: String(manifest.downloadUrl || ''),
    required: Boolean(manifest.required)
  };
});
ipcMain.handle('app:updateOpen', async (_, url) => {
  if (!/^https?:\/\//i.test(String(url || ''))) throw new Error('更新下载地址无效。');
  return shell.openExternal(String(url));
});
ipcMain.handle('app:info', () => ({
  mode: 'cloud',
  version: APP_VERSION,
  serverUrl,
  configPath,
  cachePath: tempDir,
  loginName: currentUserInfo ? currentUserInfo.displayName : '',
  updateConfigured: true
}));
