// Shared portfolio core: CSV parsing, blueprint checks, formatters, hooks.
// Exposed on window.Portfolio so each variation can pull from it.

const PALETTE = {
  bg0: '#06080b',
  bg1: '#0d1117',
  bg2: '#161b22',
  bg3: '#1f2632',
  border: '#232a36',
  borderHi: '#2f3744',
  inkHi: '#e6edf3',
  inkMid: '#9ca3af',
  inkLo: '#6b7280',
  ok: '#4ade80',
  warn: '#fbbf24',
  bad: '#f87171',
  accent: '#38bdf8',
  phosphor: '#7dd3fc',
};

// ─── CSV parse ────────────────────────────────────────────────
function parseCSV(text) {
  const rows = [];
  let row = [], cell = '', quoted = false;
  for (let i = 0; i < text.length; i++) {
    const c = text[i], n = text[i + 1];
    if (c === '"' && quoted && n === '"') { cell += '"'; i++; }
    else if (c === '"') { quoted = !quoted; }
    else if (c === ',' && !quoted) { row.push(cell); cell = ''; }
    else if ((c === '\n' || c === '\r') && !quoted) {
      if (c === '\r' && n === '\n') i++;
      row.push(cell);
      if (row.some(v => v.trim() !== '')) rows.push(row);
      row = []; cell = '';
    } else { cell += c; }
  }
  row.push(cell);
  if (row.some(v => v.trim() !== '')) rows.push(row);
  if (!rows.length) return [];
  const headers = rows[0].map(h => h.trim().toLowerCase().replace(/\s+/g, '_'));
  return rows.slice(1).map(values => {
    const o = {};
    headers.forEach((h, i) => { o[h] = (values[i] ?? '').trim(); });
    return o;
  });
}

const toNum = v => {
  const n = Number(String(v ?? '').replace(/[$,%\s]/g, ''));
  return Number.isFinite(n) ? n : 0;
};

function normalizePositions(rows) {
  return rows.map(r => ({
    ...r,
    ticker: String(r.ticker || '').trim().toUpperCase(),
    shares: toNum(r.shares),
    amount_usd: toNum(r.amount_usd || r.amount || r.value || r.market_value),
  })).filter(r => r.ticker && (r.ticker === 'CASH' || r.amount_usd > 0 || r.shares > 0));
}

function normalizeBlueprint(rows) {
  return rows.map(r => ({
    group_type: String(r.group_type || '').toLowerCase().trim(),
    group_name: String(r.group_name || '').trim(),
    target_pct: toNum(r.target_pct),
    min_pct: toNum(r.min_pct),
    max_pct: toNum(r.max_pct),
    notes: r.notes || '',
  })).filter(r => r.group_type && r.group_name);
}

// ─── Aggregation ──────────────────────────────────────────────
function groupKeys(row, field) {
  if (field === 'position') return [row.ticker || 'Uncategorized'];
  const raw = String(row[field] || 'Uncategorized').trim();
  return raw.split('|').map(s => s.trim()).filter(Boolean);
}

function sumGroup(positions, field, name) {
  const target = name.toLowerCase();
  let amt = 0;
  positions.forEach(p => {
    groupKeys(p, field).forEach(k => {
      if (k.toLowerCase() === target) amt += p.amount_usd;
    });
  });
  return amt;
}

function contributors(positions, field, name) {
  const target = name.toLowerCase();
  return positions
    .filter(p => groupKeys(p, field).some(k => k.toLowerCase() === target))
    .sort((a, b) => b.amount_usd - a.amount_usd);
}

// Run every blueprint rule. Optionally apply a what-if delta (extra dollars
// added to a single ticker with a known group mapping based on existing rows).
function evaluateBlueprint(positions, blueprint, whatIf) {
  // Build a synthetic "positions with what-if" array
  let working = positions;
  if (whatIf && whatIf.ticker && whatIf.amount) {
    const existing = positions.find(p => p.ticker === whatIf.ticker);
    if (existing) {
      working = positions.map(p => p.ticker === whatIf.ticker
        ? { ...p, amount_usd: p.amount_usd + whatIf.amount } : p);
    } else if (whatIf.template) {
      // Use a template position so we can categorize it
      working = [...positions, { ...whatIf.template, ticker: whatIf.ticker, amount_usd: whatIf.amount, shares: 0 }];
    } else {
      working = [...positions, { ticker: whatIf.ticker, amount_usd: whatIf.amount, shares: 0,
        sector: 'Uncategorized', theme: 'Uncategorized', strategy: 'Growth', risk_bucket: 'Core' }];
    }
  }
  const total = working.reduce((s, p) => s + p.amount_usd, 0);

  return blueprint.map(rule => {
    const amount = sumGroup(working, rule.group_type, rule.group_name);
    const pct = total ? (amount / total) * 100 : 0;
    let status = 'on';
    if (rule.max_pct > 0 && pct > rule.max_pct) status = 'over';
    else if (rule.min_pct > 0 && pct < rule.min_pct) status = 'under';
    const drift = pct - rule.target_pct;
    // dollars needed to bring pct back to max (over) or min (under) at current total
    let actionDollars = 0;
    if (status === 'over')  actionDollars = ((pct - rule.max_pct) / 100) * total;
    if (status === 'under') actionDollars = ((rule.min_pct - pct) / 100) * total;
    return { ...rule, amount, pct, status, drift, actionDollars, total };
  });
}

// ─── Formatters ───────────────────────────────────────────────
const fmtMoney = v => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(v || 0);
const fmtMoneySigned = v => (v >= 0 ? '+' : '−') + fmtMoney(Math.abs(v)).replace('-', '');
const fmtPct = (v, d = 1) => `${(v || 0).toFixed(d)}%`;
const fmtPctSigned = (v, d = 1) => `${v >= 0 ? '+' : '−'}${Math.abs(v || 0).toFixed(d)}pp`;
const titleCase = v => String(v).replace(/_/g, ' ').replace(/\w\S*/g, w => w[0].toUpperCase() + w.slice(1).toLowerCase());

// ─── Strict tone copy ─────────────────────────────────────────
function strictHeadline(check) {
  const name = check.group_name;
  if (check.status === 'over')  return `${name}: ${fmtPct(check.pct)}. Over your ${fmtPct(check.max_pct)} max. Trim ${fmtMoney(check.actionDollars)}.`;
  if (check.status === 'under') return `${name}: ${fmtPct(check.pct)}. Under your ${fmtPct(check.min_pct)} min. Add ${fmtMoney(check.actionDollars)}.`;
  return `${name}: ${fmtPct(check.pct)}. Inside band.`;
}

// ─── Portfolio API (serverless functions) ────────────────────

// Extract portfolio ID from URL: /p/{id}
function getPortfolioId() {
  const match = window.location.pathname.match(/^\/p\/([a-z0-9]+)$/);
  return match ? match[1] : null;
}

// Load portfolio from server
async function loadFromServer(id) {
  const r = await fetch(`/api/portfolio/load?id=${encodeURIComponent(id)}`);
  if (!r.ok) return null;
  const data = await r.json();
  if (data.error) return null;
  return {
    positions: normalizePositions(data.positions || []),
    blueprint: normalizeBlueprint(data.blueprint || []),
  };
}

// Save portfolio to server
async function saveToServer(id, state) {
  const r = await fetch('/api/portfolio/save', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ id, positions: state.positions, blueprint: state.blueprint }),
  });
  if (!r.ok) {
    const data = await r.json().catch(() => ({}));
    throw new Error(data.error || `HTTP ${r.status}`);
  }
}

// Create a new portfolio, return the ID
async function createPortfolio() {
  const r = await fetch('/api/portfolio/create', { method: 'POST' });
  const data = await r.json();
  if (data.error) throw new Error(data.error);
  return data.id;
}

// ─── Data hook: load from server or CSV ──────────────────────
const PORTFOLIO_STATE_KEY = 'pf_state_v1';

function usePortfolioData() {
  const [data, setData] = React.useState(null);
  const portfolioId = getPortfolioId();

  React.useEffect(() => {
    (async () => {
      // If we have a portfolio ID in the URL, load from server
      if (portfolioId) {
        const localKey = `pf_${portfolioId}`;
        // Try localStorage cache first for instant load
        try {
          const cached = JSON.parse(localStorage.getItem(localKey) || 'null');
          if (cached && Array.isArray(cached.positions) && Array.isArray(cached.blueprint)) {
            setData(cached);
          }
        } catch {}
        // Then fetch from server (source of truth)
        const remote = await loadFromServer(portfolioId);
        if (remote) {
          setData(remote);
          try { localStorage.setItem(localKey, JSON.stringify(remote)); } catch {}
        } else if (!data) {
          // Portfolio ID not found in database
          setData({ positions: [], blueprint: [], notFound: true });
        }
        return;
      }

      // No portfolio ID — local-only mode (for local dev)
      try {
        const persisted = JSON.parse(localStorage.getItem(PORTFOLIO_STATE_KEY) || 'null');
        if (persisted && Array.isArray(persisted.positions) && Array.isArray(persisted.blueprint)) {
          setData(persisted);
          return;
        }
      } catch {}
      const [p, b] = await Promise.all([
        fetch('/data/positions.csv').then(r => r.text()),
        fetch('/data/blueprint.csv').then(r => r.text()),
      ]);
      const positions = normalizePositions(parseCSV(p));
      const blueprint = normalizeBlueprint(parseCSV(b));
      setData({ positions, blueprint });
    })().catch(err => console.error('Data load failed', err));
  }, []);

  return data;
}

function savePortfolio(state) {
  const portfolioId = getPortfolioId();
  if (portfolioId) {
    // Save to localStorage cache (instant)
    try { localStorage.setItem(`pf_${portfolioId}`, JSON.stringify(state)); } catch {}
  } else {
    try { localStorage.setItem(PORTFOLIO_STATE_KEY, JSON.stringify(state)); } catch {}
  }
}

function resetPortfolio() {
  const portfolioId = getPortfolioId();
  if (portfolioId) {
    try { localStorage.removeItem(`pf_${portfolioId}`); } catch {}
  } else {
    try { localStorage.removeItem(PORTFOLIO_STATE_KEY); } catch {}
  }
}

// Fetch a single ticker's price via the dashboard's existing /api/prices endpoint.
// Returns { price } on success, throws on any failure (no server, network error, etc).
async function fetchTickerPrice(ticker) {
  const t = String(ticker || '').trim().toUpperCase();
  if (!t) throw new Error('ticker required');
  const r = await fetch(`/api/prices?tickers=${encodeURIComponent(t)}`, { cache: 'no-store' });
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  const data = await r.json();
  if (data && data.error) throw new Error(data.error);
  const price = data[t];
  if (price == null) throw new Error('no price returned');
  return Number(price);
}

// What-if state hook
function useWhatIf(positions) {
  const [wi, setWi] = React.useState({ ticker: '', amount: 0 });
  // Resolve ticker to a position template so we know its categorization
  const template = React.useMemo(() => {
    if (!wi.ticker) return null;
    return positions.find(p => p.ticker === wi.ticker.toUpperCase()) || null;
  }, [wi.ticker, positions]);
  const whatIf = wi.amount && wi.ticker ? { ticker: wi.ticker.toUpperCase(), amount: wi.amount, template } : null;
  return { wi, setWi, whatIf };
}

Object.assign(window, {
  PALETTE,
  parseCSV, normalizePositions, normalizeBlueprint, toNum,
  groupKeys, sumGroup, contributors, evaluateBlueprint,
  fmtMoney, fmtMoneySigned, fmtPct, fmtPctSigned, titleCase,
  strictHeadline,
  usePortfolioData, useWhatIf,
  savePortfolio, resetPortfolio, fetchTickerPrice,
  positionsToCSV, blueprintToCSV, saveToDisk,
  getPortfolioId, createPortfolio,
});

// ─── CSV serialization + save to disk ────────────────────
const POSITION_COLUMNS  = ['ticker', 'company', 'shares', 'amount_usd', 'sector', 'theme', 'strategy', 'risk_bucket', 'account', 'custom_tags', 'notes'];
const BLUEPRINT_COLUMNS = ['group_type', 'group_name', 'target_pct', 'min_pct', 'max_pct', 'notes'];

function csvEscape(v) {
  const s = String(v ?? '');
  return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s;
}

function toCSV(rows, columns) {
  const lines = [columns.join(',')];
  rows.forEach(r => { lines.push(columns.map(c => csvEscape(r[c])).join(',')); });
  return lines.join('\n') + '\n';
}

function positionsToCSV(positions) { return toCSV(positions, POSITION_COLUMNS); }
function blueprintToCSV(blueprint) { return toCSV(blueprint, BLUEPRINT_COLUMNS); }

// Save portfolio — server API if we have an ID, otherwise local server or download.
async function saveToDisk(state) {
  const portfolioId = getPortfolioId();

  // If on a /p/{id} URL, save via server API
  if (portfolioId) {
    await saveToServer(portfolioId, state);
    return { mode: 'server', saved: ['database'] };
  }

  // Local dev: try POST /api/save, fall back to download
  try {
    const r = await fetch('/api/save', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ positions: state.positions, blueprint: state.blueprint }),
    });
    if (r.ok) {
      const data = await r.json();
      if (!data.error) return { mode: 'server', saved: data.saved || ['positions.csv', 'blueprint.csv'] };
      throw new Error(data.error);
    }
    if (r.status !== 404 && r.status !== 405) throw new Error(`HTTP ${r.status}`);
  } catch {}

  downloadFile('positions.csv', positionsToCSV(state.positions));
  downloadFile('blueprint.csv', blueprintToCSV(state.blueprint));
  return {
    mode: 'download',
    saved: ['positions.csv', 'blueprint.csv'],
    message: 'Server endpoint unavailable — both CSVs downloaded. Move them into data/.',
  };
}

function downloadFile(name, content) {
  const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
  const url = URL.createObjectURL(blob);
  const link = Object.assign(document.createElement('a'), { href: url, download: name });
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}
