"use client";
import { useMemo, useState } from "react";
import type { ValuationResponse, WaccResponse } from "@/types/api";
import { deltaClass, fmtCurrency, fmtPct } from "@/lib/format";
import {
computeAdvancedDcf,
computeDcf,
computeSensitivityGrid,
type AdvancedDcfInputs,
} from "@/lib/dcf";
import { AdvancedDcfPanel } from "@/components/prism/AdvancedDcfPanel";
import { DcfResults } from "@/components/prism/DcfResults";
import { SensitivityTable } from "@/components/prism/SensitivityTable";
type Props = { data: ValuationResponse; waccData?: WaccResponse | null; seedRevenue?: number | null };
type DcfMode = "simple" | "advanced";
function pctVsCurrent(implied?: number | null, current?: number | null): number | null {
if (implied == null || current == null || current === 0) return null;
return (implied - current) / current;
}
function SummaryChip({
label,
price,
current,
currency,
accent = false,
customised = false,
}: {
label: string;
price?: number | null;
current?: number | null;
currency: string;
accent?: boolean;
customised?: boolean;
}) {
const pct = pctVsCurrent(price, current);
return (
{label}
{customised && }
{price != null && price >= 0 ? fmtCurrency(price, 2, currency) : "—"}
{pct != null && (
{fmtPct(pct, 1, true)}
)}
);
}
function MultipleRow({
label,
multiple,
price,
current,
currency,
}: {
label: string;
multiple?: number | null;
price?: number | null;
current?: number | null;
currency: string;
}) {
const pct = pctVsCurrent(price, current);
return (
{label}
{multiple != null ? `${multiple.toFixed(1)}×` : "—"}
{price != null ? fmtCurrency(price, 2, currency) : "—"}
{pct != null ? fmtPct(pct, 1, true) : "—"}
);
}
function DCFSlider({
label,
value,
min,
max,
step,
fmt,
onChange,
}: {
label: string;
value: number;
min: number;
max: number;
step: number;
fmt: (v: number) => string;
onChange: (v: number) => void;
}) {
return (
{label}
{fmt(value)}
onChange(parseFloat(e.target.value))}
/>
{fmt(min)}
{fmt(max)}
);
}
function WaccNote({ waccData }: { waccData?: WaccResponse | null }) {
if (!waccData?.available || waccData.wacc == null) return null;
return (
Market-implied WACC
{fmtPct(waccData.wacc)}
β {waccData.beta.toFixed(2)} · Rf {fmtPct(waccData.risk_free_rate)} · ERP{" "}
{fmtPct(waccData.equity_risk_premium)} · rd {fmtPct(waccData.cost_of_debt)} · t{" "}
{fmtPct(waccData.tax_rate)}
);
}
export function ValuationCard({ data, waccData, seedRevenue }: Props) {
const { dcf, ev_ebitda, ev_revenue, price_to_book, current_price } = data;
const currency = data.currency || "USD";
// Slider defaults captured once from the initial API response
const [defaults] = useState(() => ({
wacc: dcf.wacc,
terminalGrowth: dcf.terminal_growth,
projectionYears: dcf.projection_years,
growthRate: dcf.growth_rate_used ?? 0.05,
}));
const [wacc, setWacc] = useState(defaults.wacc);
const [terminalGrowth, setTerminalGrowth] = useState(defaults.terminalGrowth);
const [projectionYears, setProjectionYears] = useState(defaults.projectionYears);
const [growthRate, setGrowthRate] = useState(defaults.growthRate);
const [mode, setMode] = useState("simple");
const [advancedInputs, setAdvancedInputs] = useState(() => ({
baseRevenue: seedRevenue ?? 1_000,
revenueGrowth: [0.05, 0.05, 0.05, 0.05, 0.05],
ebitdaMargin: [0.25, 0.25, 0.25, 0.25, 0.25],
dnaPctRevenue: [0.05, 0.05, 0.05, 0.05, 0.05],
capexPctRevenue: [0.07, 0.07, 0.07, 0.07, 0.07],
nwcPctDeltaRev: [0.02, 0.02, 0.02, 0.02, 0.02],
taxRate: [0.21, 0.21, 0.21, 0.21, 0.21],
}));
const isCustomised =
mode !== "simple" ||
wacc !== defaults.wacc ||
terminalGrowth !== defaults.terminalGrowth ||
projectionYears !== defaults.projectionYears ||
growthRate !== defaults.growthRate;
function handleReset() {
setMode("simple");
setWacc(defaults.wacc);
setTerminalGrowth(defaults.terminalGrowth);
setProjectionYears(defaults.projectionYears);
setGrowthRate(defaults.growthRate);
}
// equityBridge = net_debt + preferred equity + minority interest.
// Derived as enterprise_value - equity_value (both from API); stays fixed while sliders move.
const equityBridge =
dcf.enterprise_value != null && dcf.equity_value != null
? dcf.enterprise_value - dcf.equity_value
: (dcf.net_debt ?? 0);
const computed = useMemo(() => {
if (!dcf.available || dcf.error || !data.shares_outstanding) return null;
if (mode === "advanced") {
return computeAdvancedDcf(advancedInputs, {
wacc,
terminalGrowth,
projectionYears: advancedInputs.revenueGrowth.length,
sharesOutstanding: data.shares_outstanding,
equityBridge,
});
}
if (!dcf.base_fcf) return null;
return computeDcf(
{ baseFcf: dcf.base_fcf, equityBridge, sharesOutstanding: data.shares_outstanding },
{ wacc, terminalGrowth, projectionYears, growthRate }
);
}, [mode, advancedInputs, wacc, terminalGrowth, projectionYears, growthRate, equityBridge, dcf, data.shares_outstanding]);
const livePrice =
computed && !("error" in computed) ? computed.intrinsicValuePerShare : null;
const sensitivityMatrix = useMemo(() => {
if (!dcf.available || dcf.error || !data.shares_outstanding) return null;
if (mode === "simple" && dcf.sensitivity) return dcf.sensitivity;
if (mode === "simple" && !dcf.base_fcf) return null;
const shares = data.shares_outstanding;
const grid = computeSensitivityGrid(wacc, terminalGrowth, (testWacc, testTerminalGrowth) => {
if (mode === "simple") {
if (!dcf.base_fcf) return null;
const result = computeDcf(
{ baseFcf: dcf.base_fcf, equityBridge, sharesOutstanding: shares },
{ wacc: testWacc, terminalGrowth: testTerminalGrowth, projectionYears, growthRate }
);
return "error" in result ? null : result.intrinsicValuePerShare;
}
const result = computeAdvancedDcf(advancedInputs, {
wacc: testWacc,
terminalGrowth: testTerminalGrowth,
projectionYears: advancedInputs.revenueGrowth.length,
sharesOutstanding: shares,
equityBridge,
});
return "error" in result ? null : result.intrinsicValuePerShare;
});
return {
wacc: grid.wacc,
terminal_growth: grid.terminalGrowth,
implied_prices: grid.impliedPrices,
};
}, [mode, dcf, advancedInputs, wacc, terminalGrowth, projectionYears, growthRate, equityBridge, data.shares_outstanding]);
const hasMultiples = ev_ebitda.available || ev_revenue.available || price_to_book.available;
return (
{/* Summary strip */}
Market Price
{current_price != null ? fmtCurrency(current_price, 2, currency) : "—"}
{data.currency_warning && (
{data.currency_warning}
)}
{/* DCF detail */}
Discounted Cash Flow
{dcf.available && !dcf.error && isCustomised && (
)}
{!dcf.available && (
Insufficient free cash flow history for DCF.
)}
{dcf.available && dcf.error && (
{dcf.error}
)}
{dcf.available && !dcf.error && (
{/* Left: assumptions */}
Assumptions
{mode === "simple" ? (
<>
`${(v * 100).toFixed(1)}%`}
onChange={setWacc}
/>
`${(v * 100).toFixed(2)}%`}
onChange={setTerminalGrowth}
/>
`${(v * 100).toFixed(0)}%`}
onChange={setGrowthRate}
/>
`${v} yr`}
onChange={(v) => setProjectionYears(Math.round(v))}
/>
>
) : (
)}
{/* Divider */}
{/* Right: results */}
)}
{sensitivityMatrix && (
)}
{/* Multiples */}
{hasMultiples && (
Multiples — at current market multiple
{ev_ebitda.available && (
)}
{ev_revenue.available && (
)}
{price_to_book.available && (
)}
)}
);
}