/**
 * lightweight-charts-indicators 미등록 / 확장 지표 계산
 * (업비트 차트 기준 공식)
 */
import type { OHLCVBar } from '../types';
import type { PlotData } from './indicatorRegistry';
import { applyPlotOffset, normalizeBollingerParams } from './bollingerConfig';

function sma(values: number[], period: number): number[] {
  return values.map((_, i) => {
    if (i < period - 1) return NaN;
    let sum = 0;
    for (let j = i - period + 1; j <= i; j++) sum += values[j];
    return sum / period;
  });
}

function ema(values: number[], period: number): number[] {
  const out = new Array<number>(values.length).fill(NaN);
  if (values.length < period) return out;

  // 선행 NaN(1차 EMA 워밍업 등) 이후 첫 유효 구간에서 SMA 시드
  let start = -1;
  for (let i = period - 1; i < values.length; i++) {
    let ok = true;
    for (let j = i - period + 1; j <= i; j++) {
      if (!Number.isFinite(values[j])) { ok = false; break; }
    }
    if (ok) { start = i; break; }
  }
  if (start < 0) return out;

  let sum = 0;
  for (let j = start - period + 1; j <= start; j++) sum += values[j];
  let prev = sum / period;
  out[start] = prev;
  const k = 2 / (period + 1);
  for (let i = start + 1; i < values.length; i++) {
    const v = values[i];
    if (!Number.isFinite(v)) { out[i] = NaN; continue; }
    prev = v * k + prev * (1 - k);
    out[i] = prev;
  }
  return out;
}

function sourceAt(bar: OHLCVBar, src: string): number {
  switch (src) {
    case 'open':  return bar.open;
    case 'high':  return bar.high;
    case 'low':   return bar.low;
    case 'hl2':   return (bar.high + bar.low) / 2;
    case 'hlc3':  return (bar.high + bar.low + bar.close) / 3;
    case 'ohlc4': return (bar.open + bar.high + bar.low + bar.close) / 4;
    default:      return bar.close;
  }
}

function toPlotData(bars: OHLCVBar[], values: number[]): PlotData {
  return bars.map((b, i) => ({
    time: b.time,
    value: Number.isFinite(values[i]) ? values[i] : NaN,
  }));
}

/** 업비트 이격도: 종가 ÷ 이동평균 × 100 */
export function calcDisparityUpbit(
  bars: OHLCVBar[],
  params: Record<string, number | string | boolean>,
): Record<string, PlotData> {
  const src = String(params.src ?? 'close');
  const closes = bars.map(b => sourceAt(b, src));
  const ultra = Number(params.ultraLength ?? 5);
  const short = Number(params.shortLength ?? 10);
  const mid   = Number(params.midLength ?? 20);
  const long  = Number(params.longLength ?? 60);

  const plot = (period: number, id: string) => {
    const ma = sma(closes, period);
    const vals = closes.map((c, i) =>
      Number.isFinite(ma[i]) && ma[i] !== 0 ? (c / ma[i]) * 100 : NaN,
    );
    return { [id]: toPlotData(bars, vals) };
  };

  return {
    ...plot(ultra, 'plot0'),
    ...plot(short, 'plot1'),
    ...plot(mid, 'plot2'),
    ...plot(long, 'plot3'),
  };
}

/** VR: 상승일 거래량 합 / 하락일 거래량 합 × 100 */
export function calcVR(
  bars: OHLCVBar[],
  params: Record<string, number | string | boolean>,
): Record<string, PlotData> {
  const period = Number(params.length ?? 10);
  const closes = bars.map(b => b.close);
  const volumes = bars.map(b => b.volume);
  const vals = closes.map((_, i) => {
    if (i < period) return NaN;
    let upVol = 0;
    let downVol = 0;
    for (let j = i - period + 1; j <= i; j++) {
      if (closes[j] > closes[j - 1]) upVol += volumes[j];
      else if (closes[j] < closes[j - 1]) downVol += volumes[j];
    }
    if (downVol === 0) return NaN;
    return (upVol / downVol) * 100;
  });
  return { plot0: toPlotData(bars, vals) };
}

/** 심리도(업비트): N일 중 전일 대비 상승일 비율 × 100 */
export function calcPsychological(
  bars: OHLCVBar[],
  params: Record<string, number | string | boolean>,
): Record<string, PlotData> {
  const period = Number(params.length ?? 12);
  const closes = bars.map(b => b.close);
  const vals = closes.map((_, i) => {
    if (i < period) return NaN;
    let up = 0;
    for (let j = i - period + 1; j <= i; j++) {
      if (closes[j] > closes[j - 1]) up++;
    }
    return (up / period) * 100;
  });
  return { plot0: toPlotData(bars, vals) };
}

/** @deprecated Psychological 사용 */
export const calcNewPsychological = calcPsychological;

/** 투자심리도(업비트): N일 중 상승일 거래량 비중 × 100 */
export function calcInvestPsychological(
  bars: OHLCVBar[],
  params: Record<string, number | string | boolean>,
): Record<string, PlotData> {
  const period = Number(params.length ?? 10);
  const closes = bars.map(b => b.close);
  const volumes = bars.map(b => b.volume);
  const vals = closes.map((_, i) => {
    if (i < period) return NaN;
    let upVol = 0;
    let total = 0;
    for (let j = i - period + 1; j <= i; j++) {
      total += volumes[j];
      if (closes[j] > closes[j - 1]) upVol += volumes[j];
    }
    if (total === 0) return NaN;
    return (upVol / total) * 100;
  });
  return { plot0: toPlotData(bars, vals) };
}

/**
 * TRIX + 신호선 (업비트·TradingView 동일)
 * 1) ln(종가) → 2) EMA × 3 → 3) 1봉 차분 × 10000 → 4) 신호선 = TRIX의 EMA(signalLength)
 */
export function calcTRIXWithSignal(
  bars: OHLCVBar[],
  params: Record<string, number | string | boolean>,
): Record<string, PlotData> {
  const len = Number(params.length ?? 12);
  const sigLen = Number(params.signalLength ?? 9);
  const logClose = bars.map(b => (b.close > 0 ? Math.log(b.close) : NaN));

  const e1 = ema(logClose, len);
  const e2 = ema(e1.map(v => (Number.isFinite(v) ? v : NaN)), len);
  const e3 = ema(e2.map(v => (Number.isFinite(v) ? v : NaN)), len);

  const trix = e3.map((v, i) => {
    if (i === 0 || !Number.isFinite(v) || !Number.isFinite(e3[i - 1])) return NaN;
    return (v - e3[i - 1]) * 10_000;
  });

  const signal = ema(trix.map(v => (Number.isFinite(v) ? v : NaN)), sigLen);

  return {
    plot0: toPlotData(bars, trix),
    plot1: toPlotData(bars, signal),
  };
}

/** OBV + 신호선 MA (업비트: 단순·지수·가중, 기본 단순 9) */
export function calcOBVWithMA(
  bars: OHLCVBar[],
  params: Record<string, number | string | boolean>,
): Record<string, PlotData> {
  const raw = String(params.maType ?? 'SMA');
  const maType = raw === 'EMA' || raw === 'WMA' ? raw : 'SMA';
  const maLen = Math.max(1, Number(params.maLength ?? 9) || 9);

  const obv: number[] = [];
  let acc = 0;
  for (let i = 0; i < bars.length; i++) {
    if (i === 0) {
      obv.push(0);
      continue;
    }
    const vol = bars[i].volume;
    if (bars[i].close > bars[i - 1].close) acc += vol;
    else if (bars[i].close < bars[i - 1].close) acc -= vol;
    obv.push(acc);
  }

  let ma: number[];
  switch (maType) {
    case 'EMA':
      ma = ema(obv, maLen);
      break;
    case 'WMA':
      ma = obv.map((_, i) => {
        if (i < maLen - 1) return NaN;
        let wSum = 0;
        let w = 0;
        for (let j = 0; j < maLen; j++) {
          const weight = j + 1;
          wSum += obv[i - maLen + 1 + j] * weight;
          w += weight;
        }
        return wSum / w;
      });
      break;
    default:
      ma = sma(obv, maLen);
  }

  return {
    plot0: toPlotData(bars, obv),
    plot1: toPlotData(bars, ma),
  };
}

/**
 * 표준편차 (TradingView ta.stdev / 업비트 BB)
 * - Pine v5 ta.stdev: 모집단 분산 (N으로 나눔)
 */
function stdevBb(values: number[], period: number): number[] {
  const avg = sma(values, period);
  return values.map((_, i) => {
    if (i < period - 1) return NaN;
    let sumSq = 0;
    for (let j = i - period + 1; j <= i; j++) {
      const d = values[j] - avg[i];
      sumSq += d * d;
    }
    return Math.sqrt(sumSq / period);
  });
}

/** 볼린저밴드 (업비트): SMA(종가) ± mult × σ, 오프셋 적용 */
export function calcBollingerBands(
  bars: OHLCVBar[],
  params: Record<string, number | string | boolean>,
): Record<string, PlotData> {
  const p = normalizeBollingerParams(params);
  const period = p.length as number;
  const mult = p.mult as number;
  const offset = p.offset as number;
  const src = String(p.src ?? 'close');
  const values = bars.map(b => sourceAt(b, src));

  const basis = sma(values, period);
  const dev = stdevBb(values, period).map(d => (Number.isFinite(d) ? d * mult : NaN));
  const upper = basis.map((m, i) =>
    Number.isFinite(m) && Number.isFinite(dev[i]) ? m + dev[i] : NaN,
  );
  const lower = basis.map((m, i) =>
    Number.isFinite(m) && Number.isFinite(dev[i]) ? m - dev[i] : NaN,
  );

  let plot0 = toPlotData(bars, basis);
  let plot1 = toPlotData(bars, upper);
  let plot2 = toPlotData(bars, lower);
  if (offset) {
    plot0 = applyPlotOffset(plot0, offset);
    plot1 = applyPlotOffset(plot1, offset);
    plot2 = applyPlotOffset(plot2, offset);
  }
  return { plot0, plot1, plot2 };
}

/** Wilder RMA — TradingView ta.rma / oakscriptjs 와 동일 시드·유지 */
function rmaWilder(values: Array<number | null>, period: number): number[] {
  const len = Math.max(1, Math.floor(period));
  const alpha = 1 / len;
  const out = new Array<number>(values.length).fill(NaN);
  let initSum = 0;
  let validCount = 0;
  let firstIdx = -1;
  for (let i = 0; i < values.length; i++) {
    const v = values[i];
    if (v != null && Number.isFinite(v)) {
      initSum += v;
      validCount++;
      if (validCount === len) {
        firstIdx = i;
        break;
      }
    }
  }
  let rmaVal = validCount > 0 ? initSum / validCount : NaN;
  const initialized = firstIdx >= 0;
  for (let i = 0; i < values.length; i++) {
    if (!initialized || i < firstIdx) {
      out[i] = NaN;
    } else if (i === firstIdx) {
      out[i] = rmaVal;
    } else {
      const v = values[i];
      if (v != null && Number.isFinite(v)) {
        rmaVal = alpha * v + (1 - alpha) * rmaVal;
      }
      out[i] = rmaVal;
    }
  }
  return out;
}

/** ADXR = (ADX + ADX[n]) / 2 (Wilder·업비트) */
function calcAdxr(adx: number[], lag: number): number[] {
  return adx.map((cur, i) => {
    if (!Number.isFinite(cur) || i < lag) return NaN;
    const prev = adx[i - lag];
    return Number.isFinite(prev) ? (cur + prev) / 2 : NaN;
  });
}

/**
 * DMI (업비트) — +DI, -DI, DX, ADX, ADXR
 * 파라미터: diLength(DI 길이), adxSmoothing(ADX 스무딩)
 */
export function calcDMI(
  bars: OHLCVBar[],
  params: Record<string, number | string | boolean>,
): Record<string, PlotData> {
  const diLength = Math.max(1, Number(params.diLength ?? params.length ?? 14));
  const adxSmoothing = Math.max(1, Number(params.adxSmoothing ?? 14));
  const len = bars.length;

  const plusDM: Array<number | null> = [];
  const minusDM: Array<number | null> = [];
  for (let i = 0; i < len; i++) {
    if (i === 0) {
      plusDM.push(null);
      minusDM.push(null);
    } else {
      const upMove = bars[i].high - bars[i - 1].high;
      const downMove = bars[i - 1].low - bars[i].low;
      plusDM.push(upMove > downMove && upMove > 0 ? upMove : 0);
      minusDM.push(downMove > upMove && downMove > 0 ? downMove : 0);
    }
  }

  const tr: Array<number | null> = bars.map((b, i) => {
    if (i === 0) return null;
    const hl = b.high - b.low;
    const hc = Math.abs(b.high - bars[i - 1].close);
    const lc = Math.abs(b.low - bars[i - 1].close);
    return Math.max(hl, hc, lc);
  });

  const smPlusDM = rmaWilder(plusDM, diLength);
  const smMinusDM = rmaWilder(minusDM, diLength);
  const smTR = rmaWilder(tr, diLength);

  let lastPlus = 0;
  let lastMinus = 0;
  const plusDI: number[] = [];
  const minusDI: number[] = [];
  for (let i = 0; i < len; i++) {
    const t = smTR[i];
    const pDm = smPlusDM[i];
    const mDm = smMinusDM[i];
    if (!Number.isFinite(t) || t === 0 || !Number.isFinite(pDm)) {
      plusDI.push(lastPlus);
    } else {
      const val = (100 * pDm) / t;
      plusDI.push(val);
      lastPlus = val;
    }
    if (!Number.isFinite(t) || t === 0 || !Number.isFinite(mDm)) {
      minusDI.push(lastMinus);
    } else {
      const val = (100 * mDm) / t;
      minusDI.push(val);
      lastMinus = val;
    }
  }

  const dx: number[] = plusDI.map((p, i) => {
    const m = minusDI[i];
    const sum = p + m;
    return sum === 0 ? 0 : (100 * Math.abs(p - m)) / sum;
  });

  const adx = rmaWilder(dx, adxSmoothing);
  const adxr = calcAdxr(adx, adxSmoothing);

  return {
    plot0: toPlotData(bars, plusDI),
    plot1: toPlotData(bars, minusDI),
    plot2: toPlotData(bars, dx),
    plot3: toPlotData(bars, adx),
    plot4: toPlotData(bars, adxr),
  };
}
