// financial/src/shared.jsx — cross-scope glue for the Financial Hub.
//
// Babel-standalone compiles every text/babel block into its own function
// scope, so a slice can never see the inline block's lexical bindings.
// Anything a slice AND the shell must both use lives here, published on
// window.FinanceShared. This file is the FIRST slice in document order,
// so later slices (career, retirement, …) and the inline block can safely
// destructure window.FinanceShared at module scope.
//
// The sharp edge is React context identity: DataProvider (inline block)
// and slice components must hold the SAME DataContext object, or
// useContext silently returns the empty default and every tab looks
// disconnected. It is created here, once.

const { useState, useCallback } = React;

// ── Formatters ────────────────────────────────────────────────────────────────
const fmtCur = n => new Intl.NumberFormat('en-US',{style:'currency',currency:'USD',maximumFractionDigits:0}).format(n||0);
const fmtCurFull = n => new Intl.NumberFormat('en-US',{style:'currency',currency:'USD'}).format(n||0);
const fmtPct = n => `${((n||0)*100).toFixed(1)}%`;
const fmtPctDirect = n => `${(n||0).toFixed(1)}%`;
const fmtDate = d => { try { return new Date(d).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}); } catch(e){ return d||''; } };
const fmtMonthYear = d => { try { return new Date(d).toLocaleDateString('en-US',{month:'short',year:'numeric'}); } catch(e){ return d||''; } };
const timeAgo = ts => { if(!ts) return 'never'; const s = Math.floor((Date.now()-new Date(ts.toDate?.()??ts).getTime())/1000); if(s<60) return 'just now'; if(s<3600) return `${Math.floor(s/60)}m ago`; if(s<86400) return `${Math.floor(s/3600)}h ago`; return `${Math.floor(s/86400)}d ago`; };

// `fns` comes from the plain firebase <script> in <head>, which runs before
// any babel block. Match the server's 300s budget — default httpsCallable
// timeout is 70s, which is too tight for AI endpoints (generateKidsPlan,
// strategy, tax, portfolio analysis) that legitimately take 60-120s.
const callFn = (name, data) => fns.httpsCallable(name, { timeout: 300000 })(data).then(r => r.data);

// ── Toast ─────────────────────────────────────────────────────────────────────
function useToast() {
  const [toast, setToast] = useState(null);
  const show = useCallback((msg, type='success') => {
    setToast({msg,type});
    setTimeout(()=>setToast(null), 3500);
  },[]);
  const Toast = toast ? (
    <div className={`toast ${toast.type}`}>{toast.type==='success'?'✓ ':toast.type==='error'?'✕ ':''}{toast.msg}</div>
  ) : null;
  return { show, Toast };
}

// ── Monarch sync ──────────────────────────────────────────────────────────────
// One implementation for the three Sync buttons (Dashboard, Accounts,
// Settings) — they were three hand-copied handlers that drifted on wording
// and auth handling. `onAuthError` lets Settings open its re-auth panel;
// callers without one get pointed at Settings in the toast.
const MONARCH_AUTHISH = /unauthenticated|401|403|session|token|expired/i;
function useMonarchSync({ reload, show, onAuthError }) {
  const [syncing, setSyncing] = useState(false);
  const handleSync = useCallback(async () => {
    setSyncing(true);
    try {
      const res = await callFn('syncMonarchData',{});
      reload();
      const errs = res?.data?.errors;
      if (errs?.length) {
        show(`Sync partial — ${errs.map(e=>e.section+': '+e.error).join('; ')}`, 'error');
        if (onAuthError && errs.some(e => MONARCH_AUTHISH.test(e.error||''))) onAuthError();
      } else {
        const got = `${res?.data?.transactions||0} tx · ${res?.data?.holdings||0} holdings · ${res?.data?.accounts||0} accts`;
        show(`Synced · ${got}`);
      }
    } catch(e){
      const isAuth = e.code === 'functions/unauthenticated';
      show(isAuth
        ? (onAuthError ? 'Monarch session expired — re-authenticate below'
                       : 'Monarch session expired — reconnect in Settings > Monarch')
        : e.message, 'error');
      if (isAuth && onAuthError) onAuthError();
    }
    setSyncing(false);
  }, [reload, show, onAuthError]);
  return { syncing, handleSync };
}

// ── Personal profile ──────────────────────────────────────────────────────────
// Birthdays drive age calculations everywhere so ages are never user-editable
// in the retirement planner. If this app ever becomes multi-user, promote
// these to a users/{uid}/profile Firestore doc.
const USER_PROFILE = {
  birthday: '1981-02-21',       // you
  spouseBirthday: '1984-02-09', // wife
};
// Parse a YYYY-MM-DD string as a local-calendar date (avoids the UTC-midnight
// timezone shift that drops '1984-02-09' to Feb 8 in US timezones).
const parseLocalDate = (iso) => {
  const [y, m, d] = iso.split('-').map(Number);
  return new Date(y, (m||1) - 1, d||1);
};
const calcAge = (iso, asOf = new Date()) => {
  const b = parseLocalDate(iso);
  let age = asOf.getFullYear() - b.getFullYear();
  const m = asOf.getMonth() - b.getMonth();
  if (m < 0 || (m === 0 && asOf.getDate() < b.getDate())) age--;
  return age;
};

// ── Markdown view (scoped JSX renderer, no innerHTML / no XSS surface) ────
// Renders a small markdown subset used by AI plan output: headers (# / ## /
// ### / ####), horizontal rules (---), GitHub-style tables, bullet/numbered
// lists, paragraphs, and inline **bold**, *italic*, `code`. Tuned for the
// Kids/Strategy/Tax AI plans, not a full CommonMark implementation.
function MdInline({ text }) {
  // Tokenize **bold**, *italic*, `code` and emit JSX spans.
  const out = [];
  let i = 0, key = 0;
  const re = /(\*\*[^*]+\*\*)|(`[^`]+`)|(\*[^*\s][^*]*\*)/g;
  let m;
  while ((m = re.exec(text)) !== null) {
    if (m.index > i) out.push(text.slice(i, m.index));
    if (m[1]) out.push(<strong key={key++} style={{color:'#e2e8f0',fontWeight:600}}>{m[1].slice(2,-2)}</strong>);
    else if (m[2]) out.push(<code key={key++} style={{background:'#0b1220',padding:'1px 5px',borderRadius:4,fontSize:'0.92em',color:'#10b981'}}>{m[2].slice(1,-1)}</code>);
    else if (m[3]) out.push(<em key={key++}>{m[3].slice(1,-1)}</em>);
    i = m.index + m[0].length;
  }
  if (i < text.length) out.push(text.slice(i));
  return <>{out}</>;
}

function MarkdownView({ text }) {
  if (!text) return null;
  const lines = text.split('\n');
  const blocks = [];
  let i = 0;
  while (i < lines.length) {
    const line = lines[i];
    const trimmed = line.trim();

    // Skip blanks
    if (!trimmed) { i++; continue; }

    // Horizontal rule
    if (/^-{3,}$|^\*{3,}$|^_{3,}$/.test(trimmed)) { blocks.push({ type:'hr' }); i++; continue; }

    // Heading
    const h = trimmed.match(/^(#{1,4})\s+(.+)$/);
    if (h) { blocks.push({ type:'heading', level:h[1].length, text:h[2] }); i++; continue; }

    // Table: pipe row followed by separator row
    if (trimmed.startsWith('|') && i + 1 < lines.length && /^\s*\|?[\s|:-]+\|?\s*$/.test(lines[i+1]) && lines[i+1].includes('|')) {
      const headerCells = trimmed.replace(/^\||\|$/g,'').split('|').map(s => s.trim());
      i += 2;
      const rows = [];
      while (i < lines.length && lines[i].trim().startsWith('|')) {
        const cells = lines[i].trim().replace(/^\||\|$/g,'').split('|').map(s => s.trim());
        rows.push(cells);
        i++;
      }
      blocks.push({ type:'table', header: headerCells, rows });
      continue;
    }

    // List (- or *) or numbered (1. )
    const bullet = trimmed.match(/^[-*]\s+(.+)$/);
    const numbered = trimmed.match(/^\d+\.\s+(.+)$/);
    if (bullet || numbered) {
      const ordered = !!numbered;
      const items = [];
      while (i < lines.length) {
        const t = lines[i].trim();
        const b = t.match(/^[-*]\s+(.+)$/);
        const n = t.match(/^\d+\.\s+(.+)$/);
        if (ordered ? n : b) { items.push((b||n)[1]); i++; }
        else if (!t) { i++; break; }
        else break;
      }
      blocks.push({ type:'list', ordered, items });
      continue;
    }

    // Paragraph: gather lines until blank or block boundary
    const para = [trimmed];
    i++;
    while (i < lines.length) {
      const t = lines[i].trim();
      if (!t) break;
      if (/^(#{1,4})\s/.test(t) || t.startsWith('|') || /^[-*]\s/.test(t) || /^\d+\.\s/.test(t) || /^-{3,}$/.test(t)) break;
      para.push(t);
      i++;
    }
    blocks.push({ type:'paragraph', text: para.join(' ') });
  }

  const headingStyle = (lvl) => ({
    fontSize: lvl===1 ? 18 : lvl===2 ? 15 : lvl===3 ? 13 : 12,
    fontWeight: 600,
    color: lvl<=2 ? '#e2e8f0' : '#cbd5e1',
    marginTop: lvl===1 ? 0 : (lvl===2 ? 18 : 12),
    marginBottom: 8,
    paddingBottom: lvl<=2 ? 6 : 0,
    borderBottom: lvl===2 ? '1px solid #1e293b' : 'none',
    letterSpacing: lvl<=2 ? '0.02em' : 0,
  });

  return (
    <div style={{fontSize:12,lineHeight:1.65,color:'#94a3b8'}}>
      {blocks.map((b, idx) => {
        if (b.type === 'hr') return <hr key={idx} style={{border:'none',borderTop:'1px solid #1e293b',margin:'14px 0'}}/>;
        if (b.type === 'heading') {
          const Tag = `h${Math.min(b.level,4)}`;
          return <Tag key={idx} style={headingStyle(b.level)}><MdInline text={b.text}/></Tag>;
        }
        if (b.type === 'paragraph') {
          return <p key={idx} style={{margin:'0 0 10px 0'}}><MdInline text={b.text}/></p>;
        }
        if (b.type === 'list') {
          const Tag = b.ordered ? 'ol' : 'ul';
          return (
            <Tag key={idx} style={{margin:'0 0 10px 0',paddingLeft:20}}>
              {b.items.map((it, j) => (
                <li key={j} style={{margin:'3px 0'}}><MdInline text={it}/></li>
              ))}
            </Tag>
          );
        }
        if (b.type === 'table') {
          return (
            <div key={idx} style={{overflowX:'auto',margin:'8px 0 14px 0'}}>
              <table style={{width:'100%',borderCollapse:'collapse',fontSize:11}}>
                <thead>
                  <tr style={{background:'#0b1220'}}>
                    {b.header.map((h, j) => (
                      <th key={j} style={{padding:'6px 10px',textAlign:'left',color:'#94a3b8',fontWeight:600,borderBottom:'1px solid #1e293b'}}>
                        <MdInline text={h}/>
                      </th>
                    ))}
                  </tr>
                </thead>
                <tbody>
                  {b.rows.map((row, j) => (
                    <tr key={j} style={{borderBottom:'1px solid #1e293b'}}>
                      {row.map((c, k) => (
                        <td key={k} style={{padding:'6px 10px',color:'#cbd5e1',verticalAlign:'top'}}>
                          <MdInline text={c}/>
                        </td>
                      ))}
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          );
        }
        return null;
      })}
    </div>
  );
}

// ── Data context (created once, shared across scopes) ─────────────────────────
const DataContext = React.createContext({});

// ── Error Boundary ────────────────────────────────────────────────────────────
class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { error: null }; }
  static getDerivedStateFromError(e) { return { error: e }; }
  render() {
    if (this.state.error) return (
      <div className="page"><div className="card" style={{borderColor:'#ef4444'}}>
        <div style={{color:'#ef4444',fontWeight:600,marginBottom:8}}>Render error</div>
        <div style={{fontSize:11,color:'#94a3b8'}}>{this.state.error.message}</div>
      </div></div>
    );
    return this.props.children;
  }
}

// Target comp mix ladder (Total Comp → expected Salary / IC / Cash / Stock)
const COMP_LADDER = window.COMP_LADDER = [
  {tc:300000,  salary:215000, ic:85000,  cash:76500,  stock:8500 },
  {tc:320000,  salary:225000, ic:95000,  cash:85500,  stock:9500 },
  {tc:340000,  salary:235000, ic:105000, cash:84000,  stock:21000},
  {tc:360000,  salary:245000, ic:115000, cash:92000,  stock:23000},
  {tc:400000,  salary:260000, ic:140000, cash:112000, stock:28000},
  {tc:475000,  salary:285000, ic:190000, cash:152000, stock:38000},
  {tc:550000,  salary:300000, ic:250000, cash:175000, stock:75000},
  {tc:625000,  salary:325000, ic:300000, cash:210000, stock:90000},
  {tc:750000,  salary:350000, ic:400000, cash:240000, stock:160000},
  {tc:825000,  salary:375000, ic:450000, cash:270000, stock:180000},
  {tc:900000,  salary:400000, ic:500000, cash:250000, stock:250000},
];

// Interpolate COMP_LADDER by salary → {ic, cash, stock}. IC share of salary
// grows with seniority per the ladder, matching real comp curves.
function projectCompFromSalary(salary) {
  const L = COMP_LADDER;
  if (salary <= L[0].salary) {
    const r = L[0], k = salary / L[0].salary;
    return { ic: Math.round(r.ic*k), cash: Math.round(r.cash*k), stock: Math.round(r.stock*k) };
  }
  if (salary >= L[L.length-1].salary) {
    const r = L[L.length-1], k = salary / r.salary;
    return { ic: Math.round(r.ic*k), cash: Math.round(r.cash*k), stock: Math.round(r.stock*k) };
  }
  for (let i = 0; i < L.length-1; i++) {
    const a = L[i], b = L[i+1];
    if (salary >= a.salary && salary <= b.salary) {
      const t = (salary - a.salary) / (b.salary - a.salary);
      const lerp = (x,y) => Math.round(x + (y-x)*t);
      return { ic: lerp(a.ic, b.ic), cash: lerp(a.cash, b.cash), stock: lerp(a.stock, b.stock) };
    }
  }
  return { ic: 0, cash: 0, stock: 0 };
}

// Generate projection rows for the N years after the last actual year.
// salaryGrowthPct is annual nominal salary increase. IC is derived from the
// ladder each year; cash/stock splits come from the ladder too.
function generateCompProjection(lastYear, lastSalary, salaryGrowthPct, years) {
  const out = [];
  let sal = lastSalary;
  for (let i = 1; i <= years; i++) {
    sal = Math.round(sal * (1 + (salaryGrowthPct||0)/100));
    const { cash, stock } = projectCompFromSalary(sal);
    out.push({ year: lastYear + i, baseSalary: sal, bonus: cash, rsu: stock, _projected: true });
  }
  return out;
}

// Pre-loaded historical compensation data (source: personal comp spreadsheet)
// Cash column = bonus field, Stock column = rsu field.
// Exposed on window because the Career slice needs the same merged view the
// Compensation tab shows. A bare `const` at slice scope is invisible to the
// other slices — the Career page read only the Firestore collection, found
// nothing, and told the athlete to "fill in a complete year" next to fifteen
// years of it.
const HISTORICAL_COMP = window.HISTORICAL_COMP = [
  {year:2010, baseSalary:70000,  bonus:5000,     rsu:0        },
  {year:2011, baseSalary:77100,  bonus:6000,     rsu:0        },
  {year:2012, baseSalary:77100,  bonus:10000,    rsu:0        },
  {year:2013, baseSalary:83000,  bonus:5000,     rsu:0        },
  {year:2014, baseSalary:83500,  bonus:21500,    rsu:0        },
  {year:2015, baseSalary:95000,  bonus:28000,    rsu:0        },
  {year:2016, baseSalary:141000, bonus:29000,    rsu:0        },
  {year:2017, baseSalary:142000, bonus:30000,    rsu:0        },
  {year:2018, baseSalary:143000, bonus:22000,    rsu:0        },
  {year:2019, baseSalary:143000, bonus:31500,    rsu:0        },
  {year:2020, baseSalary:148000, bonus:34323,    rsu:0        },
  {year:2021, baseSalary:150200, bonus:45000,    rsu:5000     },
  {year:2022, baseSalary:155500, bonus:45048.60, rsu:5005.40  },
  {year:2023, baseSalary:162800, bonus:55980,    rsu:6220     },
  {year:2024, baseSalary:175516, bonus:76210.20, rsu:8467.80  },
  {year:2025, baseSalary:185000, bonus:81900,    rsu:9100     },
  // The year in progress. Base is known in January; the IC that completes the
  // package is decided for THIS performance year and announced next January, so
  // the two non-salary figures are projections and are flagged as such —
  // `_icProjected` marks a real base sitting next to two estimates, which the
  // year-by-year table, the career comparison and the vesting section all have
  // to be able to see. (`_projected`, which means a wholly generated year,
  // would be wrong here and would hide the measured base.)
  //
  // The split is the ladder's, not a hand-entered pair: $97,250.77 of IC is
  // BELOW the $100k trigger, so a tenth in stock. It was recorded at a fifth,
  // which overstated the equity by ~$9,700 and priced the Jan-2026 grant at
  // $648/share against an actual near $303. Raise the IC if it is expected to
  // cross $100k — the 20% then follows from the rule instead of being asserted.
  {year:2026, baseSalary:200000, bonus:87525.69, rsu:9725.08, _icProjected:true },
];

window.FinanceShared = {
  DataContext, useToast, callFn,
  fmtCur, fmtCurFull, fmtPct, fmtPctDirect, fmtDate, fmtMonthYear, timeAgo,
  USER_PROFILE, parseLocalDate, calcAge,
  ErrorBoundary, COMP_LADDER, HISTORICAL_COMP, generateCompProjection,
  useMonarchSync, MONARCH_AUTHISH,
  MdInline, MarkdownView,
};
