/**
 * Horizon chart primitives shared by the dashboard and ticker page.
 * All colors come from the :root tokens in globals.css; the data marks
 * (--up / --down) are validated against the dark surface.
 */
import { useState } from 'react';
import type { OHLCVBar } from '../../lib/types';

function fmt(n: number, digits = 2): string {
  return n.toLocaleString('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits });
}

function signedR(r: number): string {
  return `${r > 0 ? '+' : r < 0 ? '−' : ''}${fmt(Math.abs(r))}`;
}

/* ------------------------------------------------------------------ */
/* RBar — diverging R-multiple bar growing from a centered zero line    */
/* ------------------------------------------------------------------ */

export function RBar({ r, max = 1.6 }: { r: number | null; max?: number }) {
  const pct = r == null ? 0 : Math.min(Math.abs(r) / Math.max(max, 0.01), 1) * 50;
  return (
    <div className="hz-rbar" role="img" aria-label={r == null ? 'no R value' : `${signedR(r)} R`}>
      <span className="hz-rbar-zero" />
      {r != null && (
        <span
          className="hz-rbar-fill"
          style={{
            left: r >= 0 ? '50%' : `${50 - pct}%`,
            width: `${pct}%`,
            background: r >= 0 ? 'var(--up)' : 'var(--down)',
            borderRadius: r >= 0 ? '0 4px 4px 0' : '4px 0 0 4px',
          }}
        />
      )}
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* PriceRail — stop → entry → now → gate level laid out spatially       */
/* ------------------------------------------------------------------ */

export function PriceRail({
  direction, entry, stop, target, current, scaleTo,
}: {
  direction: string;
  entry: number;
  stop: number;
  target: number;
  current: number | null;
  /** Extra prices the scale must span (e.g. every target in the ladder), so
   *  switching targets moves the target marker, not stop/entry/now. */
  scaleTo?: number[];
}) {
  const points = [entry, stop, target, ...(current != null ? [current] : []), ...(scaleTo ?? [])];
  const span = Math.max(...points) - Math.min(...points) || 1;
  const lo = Math.min(...points) - span * 0.06;
  const hi = Math.max(...points) + span * 0.06;
  const pct = (v: number) => Math.min(100, Math.max(0, ((v - lo) / (hi - lo)) * 100));
  const risk = Math.abs(entry - stop);
  const rNow = current != null && risk > 0
    ? (direction === 'long' ? current - entry : entry - current) / risk
    : null;
  const rTarget = risk > 0 ? Math.abs(target - entry) / risk : null;
  const inProfit = rNow != null && rNow >= 0;
  const progressLeft = current != null ? Math.min(pct(entry), pct(current)) : 0;
  const progressWidth = current != null ? Math.abs(pct(current) - pct(entry)) : 0;
  return (
    <div className="hz-rail" role="img" aria-label={
      `Stop ${fmt(stop)}, entry ${fmt(entry)}, now ${current != null ? fmt(current) : 'unknown'}, gate ${fmt(target)}`
    }>
      <div className="hz-rail-track" />
      <div
        className="hz-rail-risk"
        style={{ left: `${Math.min(pct(stop), pct(entry))}%`, width: `${Math.abs(pct(entry) - pct(stop))}%` }}
      />
      {current != null && (
        <div
          className="hz-rail-progress"
          style={{
            left: `${progressLeft}%`,
            width: `${progressWidth}%`,
            background: inProfit ? 'var(--up)' : 'var(--down)',
          }}
        />
      )}
      <div className="hz-rail-mark" style={{ left: `${pct(stop)}%` }}>
        <span className="hz-rail-tick" style={{ background: 'var(--down)' }} />
        <span className="hz-rail-label">
          <em>stop</em>
          <b>{fmt(stop)}</b>
          <i>−1R</i>
        </span>
      </div>
      <div className="hz-rail-mark" style={{ left: `${pct(entry)}%` }}>
        <span className="hz-rail-tick" style={{ background: 'var(--ink-2)' }} />
        <span className="hz-rail-label">
          <em>entry</em>
          <b>{fmt(entry)}</b>
        </span>
      </div>
      {current != null && (
        <div className="hz-rail-mark" style={{ left: `${pct(current)}%` }}>
          <span className="hz-rail-dot" style={{ background: inProfit ? 'var(--up)' : 'var(--down)' }} />
          <span className="hz-rail-label hz-rail-label-top">
            <em>now</em>
            <b style={{ color: inProfit ? 'var(--up-text)' : 'var(--down-text)' }}>{fmt(current)}</b>
            {rNow != null && <i>{signedR(rNow)}R</i>}
          </span>
        </div>
      )}
      <div className="hz-rail-mark" style={{ left: `${pct(target)}%` }} title="Gate level — screening only, not a take-profit">
        <span className="hz-rail-ring" />
        <span className="hz-rail-label">
          <em>gate</em>
          <b>{fmt(target)}</b>
          {rTarget != null && <i>+{fmt(rTarget, 1)}R</i>}
        </span>
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* RadarChart — score fingerprint over n axes (0–100), hover for values */
/* ------------------------------------------------------------------ */

export interface RadarAxis {
  label: string;
  value: number;
  /** Longer name for the tooltip / screen reader (falls back to label). */
  full?: string;
}

/** Canonical axis order — mirrors the backend's DIMENSIONS list
 *  (app/services/scoring_service.py). Every radar in the app must use this so
 *  fingerprint shapes stay comparable between the dashboard and ticker pages. */
const RADAR_DIMENSION_ORDER = ['technical', 'sr_quality', 'sentiment', 'fundamental', 'momentum'];

export function radarAxesFromDimensions(
  dimensions: { dimension: string; score: number }[],
): RadarAxis[] {
  const rank = (d: string) => {
    const i = RADAR_DIMENSION_ORDER.indexOf(d.toLowerCase());
    return i === -1 ? RADAR_DIMENSION_ORDER.length : i;
  };
  return [...dimensions]
    .sort((a, b) => rank(a.dimension) - rank(b.dimension) || a.dimension.localeCompare(b.dimension))
    .map((d) => ({
      label: d.dimension.length > 9 ? `${d.dimension.slice(0, 8)}.` : d.dimension,
      full: d.dimension,
      value: d.score,
    }));
}

export function RadarChart({
  axes, size = 120, labels = true,
}: { axes: RadarAxis[]; size?: number; labels?: boolean }) {
  const [hover, setHover] = useState<number | null>(null);
  const n = axes.length;
  if (n < 3) return null;
  // Label pad must fit "momentum 100" fully outside the left/right vertices.
  const pad = labels ? 96 : 10;
  const vb = size + pad * 2;
  const c = vb / 2;
  const r = size / 2;
  const angle = (i: number) => ((-90 + (i * 360) / n) * Math.PI) / 180;
  const pt = (v: number, i: number): [number, number] => {
    const a = angle(i);
    return [c + (Math.cos(a) * r * Math.max(0, Math.min(100, v))) / 100, c + (Math.sin(a) * r * Math.max(0, Math.min(100, v))) / 100];
  };
  const poly = (vals: number[]) =>
    vals.map((v, i) => pt(v, i).map((x) => x.toFixed(1)).join(',')).join(' ');
  const tip = hover !== null ? pt(axes[hover].value, hover) : null;
  return (
    <div className="hz-radar-wrap" style={{ width: vb, height: vb }}>
      <svg
        viewBox={`0 0 ${vb} ${vb}`}
        width={vb}
        height={vb}
        role="img"
        aria-label={`Score fingerprint: ${axes.map((a) => `${a.full ?? a.label} ${Math.round(a.value)}`).join(', ')} (0–100)`}
      >
        {[50, 100].map((ring) => (
          <polygon key={ring} points={poly(Array(n).fill(ring))} fill="none" stroke="var(--grid)" strokeWidth="1" />
        ))}
        {axes.map((a, i) => {
          const [x, y] = pt(100, i);
          return <line key={a.label} x1={c} y1={c} x2={x} y2={y} stroke="var(--grid)" strokeWidth="1" />;
        })}
        <polygon
          points={poly(axes.map((a) => a.value))}
          fill="var(--up)" fillOpacity="0.13"
          stroke="var(--up)" strokeWidth="2" strokeLinejoin="round"
        />
        {axes.map((a, i) => {
          const [x, y] = pt(a.value, i);
          return (
            <circle
              key={a.label} cx={x} cy={y} r={hover === i ? 4.5 : 3}
              fill="var(--up)" stroke="var(--surface)" strokeWidth="1.5"
            />
          );
        })}
        {labels && axes.map((a, i) => {
          const ang = angle(i);
          const x = c + Math.cos(ang) * (r + 13);
          const y = c + Math.sin(ang) * (r + 13);
          const anchor = Math.cos(ang) > 0.3 ? 'start' : Math.cos(ang) < -0.3 ? 'end' : 'middle';
          const dy = Math.sin(ang) < -0.3 ? -2 : Math.sin(ang) > 0.3 ? 9 : 4;
          return (
            <text key={a.label} x={x} y={y + dy} textAnchor={anchor} className="hz-radar-axis">
              {a.label} <tspan className="hz-radar-axisval">{Math.round(a.value)}</tspan>
            </text>
          );
        })}
        {/* hit targets on top — comfortably larger than the dots, keyboard-reachable */}
        {axes.map((a, i) => {
          const [x, y] = pt(a.value, i);
          return (
            <circle
              key={`hit-${a.label}`} cx={x} cy={y} r="13"
              fill="transparent" style={{ cursor: 'default', outline: 'none' }}
              tabIndex={0}
              aria-label={`${a.full ?? a.label}: ${Math.round(a.value)} of 100`}
              onMouseEnter={() => setHover(i)}
              onMouseLeave={() => setHover(null)}
              onFocus={() => setHover(i)}
              onBlur={() => setHover(null)}
            />
          );
        })}
      </svg>
      {hover !== null && tip && (
        <div
          className="hz-radar-tip"
          style={{ left: `${(tip[0] / vb) * 100}%`, top: `${(tip[1] / vb) * 100}%` }}
        >
          <b>{axes[hover].full ?? axes[hover].label}</b>
          <span className="num">{Math.round(axes[hover].value)} / 100</span>
        </div>
      )}
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* TradeChart — close path since entry + entry / stop trail / target    */
/* ------------------------------------------------------------------ */

/** Wilder ATR series matching paper_trade_service._atr_series_from_rows. */
function wilderAtrSeries(bars: OHLCVBar[], period = 14): (number | null)[] {
  const n = bars.length;
  const out: (number | null)[] = Array(n).fill(null);
  if (n < period + 1) return out;
  const tr: number[] = Array(n).fill(0);
  for (let i = 1; i < n; i++) {
    const h = bars[i].high;
    const l = bars[i].low;
    const pc = bars[i - 1].close;
    tr[i] = Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc));
  }
  let running = 0;
  for (let i = 1; i <= period; i++) running += tr[i];
  running /= period;
  let rounded = Math.round(running * 10000) / 10000;
  out[period] = rounded > 0 ? rounded : null;
  for (let j = period + 1; j < n; j++) {
    running = (running * (period - 1) + tr[j]) / period;
    rounded = Math.round(running * 10000) / 10000;
    out[j] = rounded > 0 ? rounded : null;
  }
  return out;
}

/**
 * Per-bar stop level after that bar's update — mirrors the production trail
 * ratchet so the chart can show a *moving* trail, not a flat line at the
 * current level.
 */
function buildStopPath(
  bars: OHLCVBar[],
  direction: string,
  entry: number,
  initStop: number,
  openedDate: string,
  mode: 'atr_trailing' | 'trailing' | 'fixed',
  atrMultiplier = 3,
  trailingPct = 12,
): number[] {
  const long = direction !== 'short';
  const out = bars.map(() => initStop);
  if (mode === 'fixed') return out;

  let stop = initStop;
  let anchor = entry;
  const atr = mode === 'atr_trailing' ? wilderAtrSeries(bars) : null;
  const trailFrac = trailingPct / 100;

  for (let i = 0; i < bars.length; i++) {
    const d = bars[i].date.slice(0, 10);
    if (d <= openedDate) {
      out[i] = initStop;
      continue;
    }
    if (mode === 'atr_trailing' && atr) {
      const close = bars[i].close;
      if (long) {
        anchor = Math.max(anchor, close);
        const a = atr[i];
        if (a != null) {
          const next = anchor - atrMultiplier * a;
          if (next < close) stop = Math.max(stop, next);
        }
      } else {
        anchor = Math.min(anchor, close);
        const a = atr[i];
        if (a != null) {
          const next = anchor + atrMultiplier * a;
          if (next > close) stop = Math.min(stop, next);
        }
      }
    } else {
      // % trailing: peak on high/low, ratchet only.
      if (long) {
        anchor = Math.max(anchor, bars[i].high);
        stop = Math.max(initStop, anchor * (1 - trailFrac));
      } else {
        anchor = Math.min(anchor, bars[i].low);
        stop = Math.min(initStop, anchor * (1 + trailFrac));
      }
    }
    out[i] = stop;
  }
  return out;
}

/** Violet used for Gate on the ticker chart — keep the overview chart in step. */
const GATE_STROKE = 'rgba(139, 92, 246, 0.75)';
const GATE_LABEL = 'rgba(196, 181, 253, 0.95)';

export function TradeChart({
  direction,
  entry,
  initialStop,
  target,
  bars,
  openedAt,
  exitMode = 'atr_trailing',
  atrMultiplier = 3,
  trailingPct = 12,
  currentPrice,
}: {
  direction: string;
  entry: number;
  /** Hard stop from the setup (floor for the trail). */
  initialStop: number;
  /** Screening gate level (not the live exit under trailing modes). */
  target: number;
  bars: OHLCVBar[];
  openedAt: string;
  exitMode?: 'time' | 'trailing' | 'atr_trailing' | 'target';
  atrMultiplier?: number;
  trailingPct?: number;
  /** Live mark; falls back to the latest close in the window. */
  currentPrice?: number | null;
}) {
  const openedDate = openedAt.slice(0, 10);
  const absEntry = bars.findIndex((b) => b.date.slice(0, 10) >= openedDate);
  if (bars.length < 2) return null;
  const entryAbs = absEntry === -1 ? bars.length - 1 : absEntry;
  const postCount = bars.length - entryAbs; // bars from entry through latest

  // Fixed virtual window so a brand-new trade doesn't glue entry to the right
  // edge. Entry sits at the middle until the trade fills the right half; then
  // it wanders left as more post-entry bars arrive.
  const WINDOW = 21;
  const MID = 10;
  const start = postCount <= MID + 1
    ? Math.max(0, entryAbs - MID)
    // Enough history: keep the latest WINDOW bars; entry falls where it falls.
    : Math.max(0, bars.length - WINDOW);
  // A trade older than the window entered before the first visible bar. Clamp to
  // the left edge — a negative index reads past the start of `series`/`stopPath`
  // and NaNs out the price and trail paths entirely.
  const entryBeforeWindow = entryAbs < start;
  const entryIdx = Math.max(0, entryAbs - start);
  const windowBars = bars.slice(start);
  const series = windowBars.map((b) => b.close);
  if (series.length < 2) return null;

  // Slot map: young trades keep entry near MID with empty space to the right.
  const lastIdx = series.length - 1;
  const young = postCount <= MID + 1;
  const slotOf = (i: number) => {
    if (!young) return i;
    return i + (MID - entryIdx);
  };
  const maxSlot = young ? Math.max(WINDOW - 1, slotOf(lastIdx)) : lastIdx;

  const trailMode: 'atr_trailing' | 'trailing' | 'fixed' =
    exitMode === 'atr_trailing' || exitMode === 'trailing' ? exitMode : 'fixed';
  const stopPathFull = buildStopPath(
    bars,
    direction,
    entry,
    initialStop,
    openedDate,
    trailMode,
    atrMultiplier,
    trailingPct,
  );
  const stopPath = stopPathFull.slice(start);
  const trailActive = trailMode !== 'fixed';
  // Gate is always drawn as structure (violet). Under target-exit mode it is
  // the real exit; otherwise it's screening context.
  const gateIsExit = exitMode === 'target';

  const now = currentPrice != null && Number.isFinite(currentPrice)
    ? currentPrice
    : series[lastIdx];

  const w = 560; const h = 150;
  const padL = 8; const padR = 96; const padT = 10; const padB = 12;
  const isShort = direction === 'short';
  // Always include the gate so the top of the chart isn't "open" — it's
  // screening structure, same as the ticker chart's Gate line.
  const vals = [...series, ...stopPath, entry, initialStop, now, target];
  let lo = Math.min(...vals);
  let hi = Math.max(...vals);
  const pad = (hi - lo) * 0.07 || 0.5;
  lo -= pad; hi += pad;
  const plotW = w - padL - padR;
  const plotH = h - padT - padB;
  const px = (i: number) => padL + (slotOf(i) / maxSlot) * plotW;
  const py = (v: number) => padT + plotH - ((v - lo) / (hi - lo)) * plotH;

  const inProfit = (price: number) => (isShort ? price <= entry : price >= entry);

  // Context (pre-entry) in muted ink.
  const contextPath = entryIdx > 0
    ? series
      .slice(0, entryIdx + 1)
      .map((v, i) => `${i === 0 ? 'M' : 'L'}${px(i).toFixed(1)},${py(v).toFixed(1)}`)
      .join(' ')
    : null;

  // Trade path: color each segment by whether the *end* bar is through entry
  // (not one color for the whole trade).
  const tradeSegs: { d: string; profit: boolean }[] = [];
  for (let i = entryIdx; i < lastIdx; i++) {
    const profit = inProfit(series[i + 1]);
    const d = `M${px(i).toFixed(1)},${py(series[i]).toFixed(1)}L${px(i + 1).toFixed(1)},${py(series[i + 1]).toFixed(1)}`;
    const prev = tradeSegs[tradeSegs.length - 1];
    if (prev && prev.profit === profit) prev.d += `L${px(i + 1).toFixed(1)},${py(series[i + 1]).toFixed(1)}`;
    else tradeSegs.push({ d, profit });
  }

  // Trail path from entry → now (stepped). Always drawn so young trades still
  // show a stop even before the trail has ratcheted.
  let trailPath = '';
  if (entryIdx <= lastIdx) {
    trailPath = `M${px(entryIdx).toFixed(1)},${py(stopPath[entryIdx]).toFixed(1)}`;
    for (let i = entryIdx + 1; i <= lastIdx; i++) {
      trailPath += `H${px(i).toFixed(1)}V${py(stopPath[i]).toFixed(1)}`;
    }
  }

  const liveStop = stopPath[lastIdx] ?? initialStop;
  const trailMoved = Math.abs(liveStop - initialStop) > 1e-6 * Math.max(1, Math.abs(entry));
  const nowProfit = inProfit(now);
  const nowCol = nowProfit ? 'var(--up)' : 'var(--down)';
  const labelX = w - padR + 10;
  const entryY = py(entry);
  const nowY = py(now);
  const stopY = py(initialStop);
  let trailLabelY = py(liveStop);
  const gateY = py(target);

  // Nudge overlapping labels on the right rail.
  const labelYs: { key: string; y: number }[] = [
    { key: 'entry', y: entryY },
    { key: 'now', y: nowY },
    { key: 'stop', y: stopY },
    { key: 'gate', y: gateY },
  ];
  if (trailActive && trailMoved) labelYs.push({ key: 'trail', y: trailLabelY });
  labelYs.sort((a, b) => a.y - b.y);
  for (let i = 1; i < labelYs.length; i++) {
    if (labelYs[i].y - labelYs[i - 1].y < 11) {
      labelYs[i].y = labelYs[i - 1].y + 11;
    }
  }
  const ly = Object.fromEntries(labelYs.map((l) => [l.key, l.y]));
  if (ly.trail != null) trailLabelY = ly.trail;

  return (
    <svg
      viewBox={`0 0 ${w} ${h}`}
      className="hz-tradechart"
      role="img"
      aria-label={
        `Price since entry ${fmt(entry)}, now ${fmt(now)}, stop ${fmt(initialStop)}`
        + (trailMoved ? `, trail ${fmt(liveStop)}` : '')
        + `, gate ${fmt(target)}`
      }
    >
      {/* Full-width hard stop — always visible, including for day-0 trades. */}
      <line
        x1={padL}
        x2={w - padR}
        y1={stopY}
        y2={stopY}
        stroke="var(--down)"
        strokeWidth="1.25"
        opacity="0.55"
      />
      <text x={labelX} y={(ly.stop ?? stopY) + 3.5} className="hz-lvl hz-lvl-down">
        stop {fmt(initialStop)}
      </text>

      {/* Gate (screening level) — violet, matches ticker chart */}
      <line
        x1={padL}
        x2={w - padR}
        y1={py(target)}
        y2={py(target)}
        stroke={GATE_STROKE}
        strokeWidth="1.25"
        strokeDasharray={gateIsExit ? undefined : '4 3'}
        opacity="0.85"
      />
      <text
        x={labelX}
        y={(ly.gate ?? gateY) + 3.5}
        className="hz-lvl"
        style={{ fill: GATE_LABEL }}
      >
        gate {fmt(target)}
      </text>

      {/* Entry */}
      <line x1={padL} x2={w - padR} y1={entryY} y2={entryY} stroke="var(--ink-3)" strokeWidth="1" opacity="0.5" />
      <text x={labelX} y={(ly.entry ?? entryY) + 3.5} className="hz-lvl">entry {fmt(entry)}</text>

      {/* Now (current price) */}
      <line
        x1={padL}
        x2={w - padR}
        y1={nowY}
        y2={nowY}
        stroke={nowCol}
        strokeWidth="1"
        opacity="0.45"
      />
      <text
        x={labelX}
        y={(ly.now ?? nowY) + 3.5}
        className="hz-lvl"
        style={{ fill: nowProfit ? 'var(--up-text)' : 'var(--down-text)' }}
      >
        now {fmt(now)}
      </text>

      {/* Trail path from entry (stepped) — only once it has ratcheted off the hard stop */}
      {trailActive && trailMoved && trailPath && (
        <path
          d={trailPath}
          fill="none"
          stroke="var(--down)"
          strokeWidth="1.5"
          strokeLinejoin="round"
          strokeLinecap="round"
          opacity="0.95"
        />
      )}
      {trailActive && trailMoved && (
        <text x={labelX} y={trailLabelY + 3.5} className="hz-lvl hz-lvl-down">
          trail {fmt(liveStop)}
        </text>
      )}

      {contextPath && (
        <path d={contextPath} fill="none" stroke="var(--ink-3)" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" opacity="0.65" />
      )}
      {tradeSegs.map((s, i) => (
        <path
          key={i}
          d={s.d}
          fill="none"
          stroke={s.profit ? 'var(--up)' : 'var(--down)'}
          strokeWidth="2"
          strokeLinejoin="round"
          strokeLinecap="round"
        />
      ))}
      {/* Single-bar trade: no segment yet — mark entry→now with a short stem if needed */}
      {entryIdx === lastIdx && (
        <circle cx={px(entryIdx)} cy={py(series[entryIdx])} r="2" fill="var(--ink-3)" />
      )}
      {/* Entry marker only when the entry bar is actually in the window — for an
          older trade the entry level line carries it instead. */}
      {!entryBeforeWindow && (
        <circle cx={px(entryIdx)} cy={py(entry)} r="3.5" fill="var(--ink-2)" stroke="var(--surface)" strokeWidth="1.5" />
      )}
      <circle cx={px(lastIdx)} cy={py(series[lastIdx])} r="4" fill={nowCol} stroke="var(--surface)" strokeWidth="2" />
    </svg>
  );
}
