// bank.jsx — Bank connection via PSD2 (Enable Banking AISP)
// Works in two modes:
//   • DEMO MODE: state.bank.config.backendUrl is empty → flows are simulated
//     with seeded ING transactions. UX is identical to production.
//   • LIVE MODE: backendUrl points to a Cloudflare Worker (see cloudflare-worker.js)
//     that wraps the Enable Banking API. The Worker hides the application_id +
//     RS256 private key used for JWT signing.

// ─── Institutions list (NL-focused) ─────────────────────────────
// IDs zijn Enable Banking ASPSP-namen. Bevestig de exacte spelling per bank via
// `GET <worker>/aspsps?country=NL` na deploy (zie BANK_DEPLOY.md stap 4).
const BANKS = [
  { id: 'ING',         label: 'ING',          color: '#FF6200', mark: 'I' },
  { id: 'ABN_AMRO',    label: 'ABN AMRO',     color: '#009286', mark: 'A' },
  { id: 'Rabobank',    label: 'Rabobank',     color: '#003E7E', mark: 'R' },
  { id: 'Bunq',        label: 'Bunq',         color: '#A6D63B', mark: 'B' },
  { id: 'SNS_Bank',    label: 'SNS',          color: '#0085C7', mark: 'S' },
  { id: 'ASN_Bank',    label: 'ASN',          color: '#5DAE5E', mark: 'A' },
  { id: 'Triodos',     label: 'Triodos',      color: '#1A2A3A', mark: 'T' },
  { id: 'Knab',        label: 'Knab',         color: '#E5006D', mark: 'K' },
  { id: 'Revolut',     label: 'Revolut',      color: '#000000', mark: 'R' },
];

// ─── Demo transaction seed (used when no backend) ──────────────
// Demo accounts: persoonlijk (most txs incl. salary), gezamenlijk (huur, AH),
// spaar (geen txs, alleen saldo).
const DEMO_ACCOUNTS = [
  { uid: 'demo-pers',   label: 'Persoonlijke rekening', kind: 'personal', iban: 'NL00 INGB 0123 4567 89', balance: 1284.42 },
  { uid: 'demo-shared', label: 'Gezamenlijke rekening', kind: 'shared',   iban: 'NL00 INGB 9876 5432 10', balance: 2456.10 },
  { uid: 'demo-spaar',  label: 'Spaarrekening',         kind: 'savings',  iban: 'NL00 INGB 5555 5555 55', balance: 4820.00 },
];

function buildDemoTransactions() {
  // p = persoonlijk, g = gezamenlijk
  const seed = [
    { a:'p', daysAgo: 0, amount:  -3.50, description: 'AH TO GO STATION CS' },
    { a:'p', daysAgo: 0, amount:  -4.20, description: 'BAGELS & BEANS' },
    { a:'g', daysAgo: 1, amount: -42.30, description: 'ALBERT HEIJN 1235 UTRECHT' },
    { a:'p', daysAgo: 1, amount:  -9.50, description: 'NS - REIZIGER' },
    { a:'p', daysAgo: 2, amount: -12.00, description: 'TIKKIE - LUNCH WERK' },
    { a:'p', daysAgo: 2, amount: -19.99, description: 'H&M ONLINE' },
    { a:'p', daysAgo: 3, amount:  -8.50, description: 'UBER UTRECHT' },
    { a:'g', daysAgo: 3, amount: -38.40, description: 'JUMBO 3201 UTRECHT' },
    { a:'p', daysAgo: 4, amount: -24.00, description: 'PATHE BIOSCOOP' },
    { a:'p', daysAgo: 4, amount:  -6.40, description: 'COFFEE COMPANY' },
    { a:'g', daysAgo: 5, amount: -65.00, description: 'BCC ELECTRO' },
    { a:'g', daysAgo: 7, amount: -17.99, description: 'SPOTIFY AB' },
    { a:'g', daysAgo: 12, amount: -850.00, description: 'HUUR - VESTIA' },
    { a:'p', daysAgo: 20, amount: 2450.00, description: 'SALARIS WERKGEVER BV' },
    { a:'p', daysAgo: 6, amount:  +25.00, description: 'TIKKIE - SARA RETOURT' },
  ];
  const accountUidFor = { p: 'demo-pers', g: 'demo-shared' };
  return seed.map((t, i) => ({
    id: 'demo-' + i,
    accountUid: accountUidFor[t.a],
    daysAgo: t.daysAgo,
    date: dateNDaysAgo(t.daysAgo),
    amount: t.amount,
    description: t.description,
    cat: isIncome(t.description) && t.amount > 0 ? 'income' : null,
  }));
}

function dateNDaysAgo(n) {
  const d = new Date(TODAY);
  d.setDate(d.getDate() - n);
  return d.toISOString().slice(0, 10);
}

// ─── Default merchant→category hints (only applied if user has NO rule yet) ─
// We deliberately keep this minimal so Maud trains her own rules.
// On sync, we leave transactions cat=null UNLESS a user rule matches.
const CAT_KEYWORDS = {};

function autoCategorize() { return null; }  // legacy

// Strip noise from a bank description to derive a 'merchant' label that's
// good as a recurring rule key. "ALBERT HEIJN 1235 UTRECHT" → "ALBERT HEIJN".
function normalizeMerchant(description) {
  if (!description) return '';
  let s = description.toUpperCase();
  // Strip common location/transaction noise
  s = s.replace(/\b\d{2,}\b/g, '');                 // remove digit blobs
  s = s.replace(/\b(UTRECHT|AMSTERDAM|ROTTERDAM|DEN HAAG|EINDHOVEN|GRONINGEN|TILBURG|ALMERE|BREDA|NIJMEGEN|ENSCHEDE|HAARLEM|ARNHEM|AMERSFOORT|MAASTRICHT|LEIDEN|DORDRECHT|ZWOLLE)\b/g, '');
  s = s.replace(/\bNL\b|\bAB\b|\bBV\b/g, '');
  s = s.replace(/[^A-Z& ]/g, ' ');
  s = s.replace(/\s+/g, ' ').trim();
  // Take first 2-3 words
  const parts = s.split(' ').filter(Boolean).slice(0, 3);
  return parts.join(' ');
}

function isIncome(description) {
  return /salaris/i.test(description) || /salary/i.test(description);
}

// Apply user-defined rules to a transaction list. Returns new array.
function applyRules(txs, rules) {
  if (!rules || rules.length === 0) return txs;
  return txs.map(t => {
    if (t.cat) return t;  // already categorized (manually or pre-tagged)
    const r = matchRule(t.description, rules);
    return r ? { ...t, cat: r.cat, ruleId: r.id } : t;
  });
}

// ─── API: call backend worker if configured, else mock ──────────
async function bankApi(state, path, opts = {}) {
  const backendUrl = state.bank?.config?.backendUrl;
  if (!backendUrl) throw new Error('demo-mode');
  const r = await fetch(backendUrl.replace(/\/$/, '') + path, {
    method: opts.method || 'GET',
    headers: { 'Content-Type': 'application/json' },
    body: opts.body ? JSON.stringify(opts.body) : undefined,
  });
  if (!r.ok) {
    const text = await r.text().catch(() => '');
    throw new Error(text || `Backend ${r.status}`);
  }
  return await r.json();
}

// ─── Hook: useBank ───────────────────────────────────────────────
function useBank() {
  const [state, set] = useDashboard();
  const [working, setWorking] = React.useState(null);   // 'connecting' | 'syncing' | null
  const [error, setError] = React.useState(null);

  const bank = state.bank || {};
  const isConnected = !!bank.connected;
  const isDemoMode = !bank.config?.backendUrl;

  // Handle return from bank OAuth redirect (live mode only)
  React.useEffect(() => {
    const url = new URL(window.location.href);
    const bankStatus = url.searchParams.get('bank');
    if (!bankStatus) return;

    let pending = null;
    try { pending = JSON.parse(localStorage.getItem('maud-pending-bank') || 'null'); } catch (e) {}

    if (bankStatus === 'connected') {
      const accountIds = (url.searchParams.get('account') || '').split(',').filter(Boolean);
      if (accountIds.length > 0) {
        set(s => {
          const existing = s.bank?.accounts || [];
          // Maak placeholder accounts voor uids die we nog niet kennen.
          // Eerste account = persoonlijk default, rest = overig (Maud labelt zelf).
          const accounts = accountIds.map((uid, i) => {
            const found = existing.find(a => a.uid === uid);
            if (found) return found;
            return {
              uid,
              label: i === 0 ? 'Persoonlijke rekening' : `Rekening ${i + 1}`,
              kind: i === 0 ? 'personal' : 'other',
              iban: null,
              balance: null,
              sharePct: null, // gebruikt default per kind
            };
          });
          return {
            ...s,
            bank: {
              ...(s.bank || {}),
              connected: true,
              institution: pending?.institutionId || s.bank?.institution,
              institutionLabel: pending?.institutionLabel || s.bank?.institutionLabel,
              accounts,
              accountId: accountIds.join(','),    // legacy / Worker reads this
              consentExpiresAt: new Date(Date.now() + 90*24*3600*1000).toISOString(),
              transactions: s.bank?.transactions || [],
            },
          };
        });
        setTimeout(() => sync(accountIds.join(',')), 600);
      }
    } else if (bankStatus === 'failed') {
      setError('Bank koppeling mislukt — probeer opnieuw');
    }

    localStorage.removeItem('maud-pending-bank');
    url.searchParams.delete('bank');
    url.searchParams.delete('account');
    url.searchParams.delete('reason');
    window.history.replaceState({}, '', url.toString());
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const connect = React.useCallback(async (institutionId, institutionLabel) => {
    setWorking('connecting'); setError(null);
    try {
      if (isDemoMode) {
        // simulate redirect → bank login → callback met 3 accounts
        await new Promise((r) => setTimeout(r, 1600));
        set(s => ({
          ...s,
          bank: {
            ...(s.bank || {}),
            connected: true,
            institution: institutionId,
            institutionLabel,
            accounts: DEMO_ACCOUNTS.map(a => ({ ...a })),
            // Legacy:
            accountId: DEMO_ACCOUNTS.map(a => a.uid).join(','),
            accountIban: DEMO_ACCOUNTS[0].iban,
            balance: DEMO_ACCOUNTS.reduce((s, a) => s + a.balance, 0),
            consentExpiresAt: new Date(Date.now() + 90*24*3600*1000).toISOString(),
            lastSync: null,
            transactions: [],
          },
        }));
        // auto sync after connect
        setTimeout(() => sync(), 500);
      } else {
        // LIVE: ask backend for a requisition redirect URL
        localStorage.setItem('maud-pending-bank', JSON.stringify({ institutionId, institutionLabel }));
        const data = await bankApi(state, '/connect', {
          method: 'POST',
          body: { institutionId, redirectUrl: window.location.href.split('?')[0] },
        });
        // Open the bank login flow
        window.location.href = data.redirectUrl;
      }
    } catch (e) {
      setError(e.message || String(e));
    } finally {
      setWorking(null);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isDemoMode, state.bank?.config?.backendUrl, set]);

  const sync = React.useCallback(async (overrideAccountId) => {
    setWorking('syncing'); setError(null);
    try {
      const useAccountId = overrideAccountId || bank.accountId;
      let txs;
      let balanceBreakdown = null;
      if (isDemoMode) {
        await new Promise((r) => setTimeout(r, 900));
        txs = buildDemoTransactions();
      } else {
        const data = await bankApi(state, `/transactions?accountId=${encodeURIComponent(useAccountId)}`);
        txs = (data.transactions || []).map(t => ({
          id: t.transactionId,
          accountUid: t.accountUid || null,
          date: t.bookingDate,
          daysAgo: Math.floor((TODAY - new Date(t.bookingDate)) / 86400000),
          amount: parseFloat(t.transactionAmount?.amount || 0),
          description: t.remittanceInformationUnstructured || t.creditorName || t.debtorName || 'transactie',
          cat: isIncome(t.remittanceInformationUnstructured || t.creditorName || '') && parseFloat(t.transactionAmount?.amount || 0) > 0 ? 'income' : null,
        }));
      }
      // Apply learned rules
      txs = applyRules(txs, state.categoryRules || []);
      // Salary detectie (alleen op persoonlijke accounts in multi-account modus)
      const personalUids = (state.bank?.accounts || []).filter(a => a.kind === 'personal').map(a => a.uid);
      const eligibleForSalary = txs.filter(t => t.amount > 0 && t.cat === 'income' && (personalUids.length === 0 || personalUids.includes(t.accountUid)));
      const salary = eligibleForSalary.sort((a, b) => a.daysAgo - b.daysAgo)[0];
      const derivedResetDay = salary?.date
        ? Math.min(28, Math.max(1, new Date(salary.date).getDate()))
        : null;

      // Saldo per rekening (live mode)
      if (!isDemoMode) {
        try {
          const bal = await bankApi(state, `/balance?accountId=${encodeURIComponent(useAccountId)}`);
          balanceBreakdown = bal?.breakdown || null;
        } catch (e) { /* non-fatal */ }
      }

      set(s => {
        // Merge balance breakdown into accounts[]. De Worker verrijkt nu met
        // iban + name + kindGuess uit EB metadata, dus we kunnen auto-labelen.
        const labelFromKind = (k) => k === 'shared' ? 'Gezamenlijke rekening'
          : k === 'personal' ? 'Persoonlijke rekening'
          : k === 'savings' ? 'Spaarrekening'
          : 'Rekening';
        const isAutoLabel = (label) => !label
          || label === 'Persoonlijke rekening'
          || label === 'Gezamenlijke rekening'
          || label === 'Spaarrekening'
          || label === 'Rekening'
          || /^Rekening \d+$/.test(label);
        let accounts = s.bank?.accounts || [];
        if (balanceBreakdown) {
          accounts = accounts.map(a => {
            const found = balanceBreakdown.find(b => b.accountUid === a.uid);
            if (!found) return a;
            const next = { ...a, balance: found.balance };
            if (found.iban && !a.iban) next.iban = found.iban;
            // Als label nog auto is → de gebruiker heeft 'm niet zelf gezet,
            // dus we mogen óók kind herzien op basis van de Worker-metadata.
            if (found.kindGuess && isAutoLabel(a.label)) {
              next.kind = found.kindGuess;
              next.label = labelFromKind(found.kindGuess);
            }
            return next;
          });
          // Add unknown accounts that came back in breakdown but weren't in state
          balanceBreakdown.forEach(b => {
            if (!accounts.find(a => a.uid === b.accountUid)) {
              const kind = b.kindGuess || 'other';
              accounts.push({
                uid: b.accountUid,
                label: labelFromKind(kind),
                kind,
                iban: b.iban || null,
                balance: b.balance,
                sharePct: null,
              });
            }
          });
        }
        const totalBal = accounts.reduce((sum, a) => sum + (typeof a.balance === 'number' ? a.balance : 0), 0);
        return {
          ...s,
          income: (s.incomeAutoFromBank !== false && salary)
            ? Math.round(salary.amount)
            : s.income,
          cycleResetDay: (s.incomeAutoFromBank !== false && derivedResetDay)
            ? derivedResetDay
            : s.cycleResetDay,
          bank: {
            ...(s.bank || {}),
            accounts,
            transactions: txs,
            balance: totalBal || s.bank?.balance,
            lastSync: new Date().toISOString(),
          },
        };
      });
    } catch (e) {
      setError(e.message || String(e));
    } finally {
      setWorking(null);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isDemoMode, state.bank?.config?.backendUrl, bank.accountId, set]);

  const disconnect = React.useCallback(() => {
    set(s => ({
      ...s,
      bank: {
        ...(s.bank || {}),
        connected: false,
        institution: null,
        institutionLabel: null,
        accounts: [],
        accountIban: null,
        accountId: null,
        balance: null,
        consentExpiresAt: null,
        lastSync: null,
        transactions: [],
      },
    }));
  }, [set]);

  // Update one account's metadata (label / kind / sharePct)
  const updateAccount = React.useCallback((uid, patch) => {
    set(s => ({
      ...s,
      bank: {
        ...(s.bank || {}),
        accounts: (s.bank?.accounts || []).map(a => a.uid === uid ? { ...a, ...patch } : a),
      },
    }));
  }, [set]);

  // Categorize a single transaction manually. Optionally learn a rule.
  const recategorize = React.useCallback((txId, newCat, opts = {}) => {
    set(s => {
      let rules = s.categoryRules || [];
      let next = (s.bank?.transactions || []).map(t => t.id === txId ? { ...t, cat: newCat } : t);
      if (opts.learn && opts.merchant) {
        // Add or update rule
        const exists = rules.find(r => r.match === opts.merchant);
        if (exists) {
          rules = rules.map(r => r.match === opts.merchant ? { ...r, cat: newCat } : r);
        } else {
          rules = [...rules, { id: 'rule-' + Date.now(), match: opts.merchant, cat: newCat, merchant: opts.merchant }];
        }
        // Apply to all other matching uncategorized tx of the same merchant
        next = next.map(t => {
          if (t.id === txId) return t;
          if (t.cat) return t;
          const d = (t.description || '').toLowerCase();
          return d.includes(opts.merchant.toLowerCase()) ? { ...t, cat: newCat } : t;
        });
      }
      return { ...s, categoryRules: rules, bank: { ...(s.bank || {}), transactions: next } };
    });
  }, [set]);

  return {
    bank, isConnected, isDemoMode, working, error,
    connect, sync, disconnect, recategorize, updateAccount,
    banks: BANKS,
  };
}

Object.assign(window, {
  useBank, BANKS, normalizeMerchant, buildDemoTransactions, applyRules, DEMO_ACCOUNTS,
});
