// AI module — talks to any OpenAI-compatible chat completions endpoint (apiyi by default).
// Two functions are exposed: parseExpenseAI and parseCreditPdfAI.

function aiHeaders(settings) {
  return {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${settings.aiKey || ''}`,
  };
}

function aiUrl(settings, path) {
  const base = (settings.aiBaseUrl || 'https://api.apiyi.com/v1').replace(/\/$/, '');
  return base + path;
}

function aiAvailable(settings) {
  return !!(settings && settings.aiEnabled && settings.aiKey);
}

// ─── Expense parsing (text → {amount, catId, note, suggestNew?}) ────
async function parseExpenseAI(text, settings) {
  const validIds = DATA.categories.filter(c => !c.archived).map(c => c.id);
  const cats = DATA.categories.filter(c => !c.archived).map(c => `"${c.id}" (${c.title})`).join(', ');
  const sys =
`Ты разбираешь короткие фразы о тратах на русском и возвращаешь СТРОГО JSON.
Доступные категории (используй только эти id): ${cats}.

Правила:
- catId — ровно один id из списка выше. Выбирай максимально точно по сути траты.
- Если ни одна категория не подходит даже приблизительно — поставь catId="other" И добавь поле suggestNew: {"title": "Название", "emoji": "🐾"} с предложением создать новую категорию (1-2 слова, без эмодзи в title; emoji — один подходящий эмодзи).
- Если хоть какая-то категория подходит — suggestNew НЕ добавляй.
- amount — число рублей (без валюты, без пробелов). Если суммы нет — null.
- note — короткая суть траты, до 40 символов, без чисел и валюты, с маленькой буквы.

Формат ответа — только JSON, без markdown, без комментариев:
{"amount": number|null, "catId": string, "note": string, "suggestNew"?: {"title": string, "emoji": string}}`;

  const res = await fetch(aiUrl(settings, '/chat/completions'), {
    method: 'POST',
    headers: aiHeaders(settings),
    body: JSON.stringify({
      model: settings.aiModel || 'claude-haiku-4-5',
      messages: [
        { role: 'system', content: sys },
        { role: 'user',   content: text },
      ],
      temperature: 0.1,
      max_tokens: 250,
    }),
  });
  if (!res.ok) throw new Error(`AI ${res.status} ${await res.text().catch(() => '')}`);
  const j = await res.json();
  const content = j.choices?.[0]?.message?.content || '{}';
  const clean = content.replace(/^[^{]*/, '').replace(/[^}]*$/, '').trim();
  const parsed = JSON.parse(clean);
  if (parsed.catId && !validIds.includes(parsed.catId)) parsed.catId = 'other';
  return parsed;
}

// ─── Credit PDF parsing ─────────────────────────────────
// Uses the file's text content (text-layer PDFs only — we don't OCR images).
// For image-PDFs the caller can switch to a multimodal model that accepts
// base64 images; we pass the base64 too so the model can use either.
async function parseCreditPdfAI(file, settings) {
  const [text, b64] = await Promise.all([extractPdfText(file), fileToBase64(file)]);

  const sys =
`Ты разбираешь договоры кредита/займа на русском и возвращаешь СТРОГО JSON.
Извлеки ключевые параметры:
- total — сумма кредита в рублях (число, без валюты).
- rate — годовая ставка в процентах (число).
- monthly — ежемесячный платёж в рублях (число).
- years — срок кредита в годах (число, дробное допустимо).
- nextDate — дата первого/ближайшего платежа в формате "DD MMM" по-русски ("05 нояб"), либо null.
- bank — название банка/кредитора (короткая строка).
- title — короткое название кредита (1-3 слова).
- schedule — необязательный массив платежей: [{date: "DD.MM.YYYY", amount: number}].

Если значения нет — null. Только JSON, без markdown.
{"total": number|null, "rate": number|null, "monthly": number|null, "years": number|null, "nextDate": string|null, "bank": string|null, "title": string|null, "schedule"?: [{date:string, amount:number}]}`;

  const userContent = text
    ? `Текст договора (фрагмент):\n${text.slice(0, 12000)}`
    : 'Текст не удалось извлечь — извлеки данные из изображения договора (base64 ниже).';

  const messages = [{ role: 'system', content: sys }];
  if (text) {
    messages.push({ role: 'user', content: userContent });
  } else {
    // Multimodal fallback — pass image part if model supports it.
    messages.push({
      role: 'user',
      content: [
        { type: 'text', text: userContent },
        { type: 'image_url', image_url: { url: `data:application/pdf;base64,${b64}` } },
      ],
    });
  }

  // For PDF/image use the multimodal model (defaults to Sonnet).
  const model = settings.aiModelMultimodal || settings.aiModel || 'claude-sonnet-4-5';
  const res = await fetch(aiUrl(settings, '/chat/completions'), {
    method: 'POST',
    headers: aiHeaders(settings),
    body: JSON.stringify({
      model,
      messages,
      temperature: 0.1,
      max_tokens: 1500,
    }),
  });
  if (!res.ok) throw new Error(`AI ${res.status} ${await res.text().catch(() => '')}`);
  const j = await res.json();
  const content = j.choices?.[0]?.message?.content || '{}';
  const clean = content.replace(/^[^{]*/, '').replace(/[^}]*$/, '').trim();
  return JSON.parse(clean);
}

// ─── PDF text extractor (text-layer only, no OCR) ──────
// Naïve: parse the raw bytes for tokens between BT/ET operators.
// Works for digitally generated bank PDFs that ship a text layer.
async function extractPdfText(file) {
  try {
    const buf = await file.arrayBuffer();
    const bytes = new Uint8Array(buf);
    // Decode as latin-1 so we can find text operators without breaking bytes.
    let s = '';
    for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);

    const out = [];
    // Match strings in parentheses inside BT…ET text blocks: `(text) Tj` / `[(text)] TJ`.
    const re = /BT([\s\S]*?)ET/g;
    let m;
    while ((m = re.exec(s)) !== null) {
      const block = m[1];
      const strRe = /\(((?:\\.|[^()\\])*)\)/g;
      let sm;
      while ((sm = strRe.exec(block)) !== null) {
        let t = sm[1]
          .replace(/\\\(/g, '(')
          .replace(/\\\)/g, ')')
          .replace(/\\\\/g, '\\')
          .replace(/\\n/g, '\n')
          .replace(/\\r/g, '\r')
          .replace(/\\t/g, '\t');
        // Heuristic CP1251-decode for cyrillic bytes (most RU bank PDFs).
        t = decodeCp1251Maybe(t);
        if (t.trim()) out.push(t);
      }
    }
    return out.join(' ');
  } catch {
    return '';
  }
}

function decodeCp1251Maybe(t) {
  // Map common cp1251 → unicode if input looks like cp1251 cyrillic.
  let hasHighByte = false;
  for (let i = 0; i < t.length; i++) if (t.charCodeAt(i) > 127) { hasHighByte = true; break; }
  if (!hasHighByte) return t;
  const map = {
    0xc0:'А',0xc1:'Б',0xc2:'В',0xc3:'Г',0xc4:'Д',0xc5:'Е',0xc6:'Ж',0xc7:'З',0xc8:'И',0xc9:'Й',
    0xca:'К',0xcb:'Л',0xcc:'М',0xcd:'Н',0xce:'О',0xcf:'П',0xd0:'Р',0xd1:'С',0xd2:'Т',0xd3:'У',
    0xd4:'Ф',0xd5:'Х',0xd6:'Ц',0xd7:'Ч',0xd8:'Ш',0xd9:'Щ',0xda:'Ъ',0xdb:'Ы',0xdc:'Ь',0xdd:'Э',
    0xde:'Ю',0xdf:'Я',
    0xe0:'а',0xe1:'б',0xe2:'в',0xe3:'г',0xe4:'д',0xe5:'е',0xe6:'ж',0xe7:'з',0xe8:'и',0xe9:'й',
    0xea:'к',0xeb:'л',0xec:'м',0xed:'н',0xee:'о',0xef:'п',0xf0:'р',0xf1:'с',0xf2:'т',0xf3:'у',
    0xf4:'ф',0xf5:'х',0xf6:'ц',0xf7:'ч',0xf8:'ш',0xf9:'щ',0xfa:'ъ',0xfb:'ы',0xfc:'ь',0xfd:'э',
    0xfe:'ю',0xff:'я',
    0xa8:'Ё',0xb8:'ё',0xb0:'°',0x85:'…',0x96:'–',0x97:'—',
  };
  let out = '';
  for (let i = 0; i < t.length; i++) {
    const c = t.charCodeAt(i);
    out += (c > 127 && map[c]) || String.fromCharCode(c);
  }
  return out;
}

function fileToBase64(file) {
  return new Promise((res, rej) => {
    const r = new FileReader();
    r.onload = () => {
      const s = String(r.result || '');
      const comma = s.indexOf(',');
      res(comma >= 0 ? s.slice(comma + 1) : s);
    };
    r.onerror = () => rej(r.error);
    r.readAsDataURL(file);
  });
}

Object.assign(window, { aiAvailable, parseExpenseAI, parseCreditPdfAI, extractPdfText });
