import type { Stock } from './types';

/**
 * 멀티팩터 합성 점수 (0~100) — `/stocks discover` 추천 순위의 코어.
 *
 * v2.2.266 도입 배경: 기존 순위는 "통과 키워드 수" 이진 카운트였는데,
 *   ① ROE 10.1%와 35%가 같은 1점 (그라데이션 없음)
 *   ② 영업이익률 하나가 성장성·수익성·영업효율 3키워드에 중복 산입 (한 지표 과대반영)
 *   ③ 저PBR 무조건 가산 → value trap (싸기만 한 부실주) 무방비
 * 를 해결하기 위해 업계 표준(Stockopedia StockRanks·S&P QVM 멀티팩터 지수) 방식인
 * **후보군 내 백분위 랭크 정규화 → Quality/Value/Momentum/Stability 4축 → 가중 합성**
 * 으로 교체한다. 저PBR 가산은 Piotroski F-Score 방식의 품질 가드를 통과할 때만 유효.
 *
 * 축별 점수·합성은 전부 결정론(코드) — LLM 은 서술만 담당한다(v2.2.211 judge 철학과 동일).
 */

export interface MomentumInputs {
    /** 약 12개월 수익률 (배수-1, 예: 0.35 = +35%). 시세 최초~최후 종가 기반. */
    return12m?: number;
    /** 224일선 회복 패턴 — passed / notApplicable(추세 이미 확립) / fail. */
    ma224?: 'passed' | 'na' | 'fail';
    /** 낙폭과대+반등초입 패턴 통과 여부. */
    dropRecovery?: boolean;
    /** 이평선 배열. */
    alignment?: '정배열' | '역배열' | '혼조';
    /** RSI(14) 분류. */
    rsi?: '과열' | '중립' | '침체';
}

export interface ScoreInputs {
    symbol: string;
    /** 가중치 결정용 투자성향 — 미지정 시 등가중. */
    style?: Stock['투자성향'];
    // Quality
    roe?: number;
    operatingMargin?: number;
    revenueGrowthYoY?: number;
    opProfitGrowthYoY?: number;
    debtRatio?: number;          // lower-better
    // Value
    per?: number;                // lower-better
    pbr?: number;                // lower-better
    // Stability
    retentionRatio?: number;
    marketCapEok?: number;
    // Momentum
    momentum?: MomentumInputs;
}

export interface AxisWeights { q: number; v: number; m: number; s: number }

export interface CompositeScore {
    symbol: string;
    /** 0~100 종합 (투자성향 가중). */
    total: number;
    quality: number | undefined;
    value: number | undefined;
    momentum: number | undefined;
    stability: number | undefined;
    /** value trap 가드 발동 여부 — 품질 미달로 Value 축이 50점에 캡핑됨. */
    valueTrapGuarded: boolean;
    /** mini F-Score "통과/평가가능" (예: "3/4"). 데이터 부족 시 undefined. */
    fScore?: string;
    /** 실제 적용된 가중치 (결측 축 재정규화 후). */
    weights: AxisWeights;
    /** "Q92 V81 M76 S88" 형태 — 출력·pitch 용. */
    breakdown: string;
}

/** 투자성향별 가중치 — 스윙=모멘텀↑, 장기=퀄리티↑, 저평가우량주=밸류↑. */
const STYLE_WEIGHTS: Record<string, AxisWeights> = {
    '스윙/중기': { q: 0.25, v: 0.25, m: 0.35, s: 0.15 },
    '장기투자': { q: 0.40, v: 0.20, m: 0.15, s: 0.25 },
    '저평가우량주': { q: 0.30, v: 0.40, m: 0.10, s: 0.20 },
};
const EQUAL_WEIGHTS: AxisWeights = { q: 0.25, v: 0.25, m: 0.25, s: 0.25 };

/**
 * 후보군 내 백분위 랭크 (0~100, 동점은 평균 랭크). values 에 없는(undefined) 후보는
 * 랭킹에서 제외 — 결측을 0점 취급하면 데이터 없는 종목이 부당하게 밀리므로.
 * n==1 이면 비교 대상이 없어 중립 50.
 */
export function percentileRanks(values: (number | undefined)[], lowerBetter = false): (number | undefined)[] {
    const present = values
        .map((v, i) => ({ v, i }))
        .filter((x): x is { v: number; i: number } => x.v !== undefined && Number.isFinite(x.v));
    const out: (number | undefined)[] = values.map(() => undefined);
    const n = present.length;
    if (n === 0) return out;
    if (n === 1) { out[present[0].i] = 50; return out; }
    const sorted = [...present].sort((a, b) => a.v - b.v);
    // 동점 평균 랭크
    const rankByIdx = new Map<number, number>();
    let k = 0;
    while (k < n) {
        let j = k;
        while (j + 1 < n && sorted[j + 1].v === sorted[k].v) j++;
        const avgRank = (k + j) / 2;
        for (let t = k; t <= j; t++) rankByIdx.set(sorted[t].i, avgRank);
        k = j + 1;
    }
    for (const { i } of present) {
        const r = rankByIdx.get(i)!;
        const p = (r / (n - 1)) * 100;
        out[i] = lowerBetter ? 100 - p : p;
    }
    return out;
}

/** 사용 가능한 하위 점수들의 평균 — 전부 결측이면 undefined (축 자체를 결측 처리). */
function axisMean(parts: (number | undefined)[]): number | undefined {
    const avail = parts.filter((p): p is number => p !== undefined);
    if (avail.length === 0) return undefined;
    return avail.reduce((a, b) => a + b, 0) / avail.length;
}

/** 모멘텀 하위 신호 → 0~100 점수 변환 (기술 신호는 등급형이라 백분위 대신 고정 매핑). */
function momentumSubScores(m: MomentumInputs | undefined, return12mPct: number | undefined): (number | undefined)[] {
    if (!m && return12mPct === undefined) return [];
    const alignScore = m?.alignment === '정배열' ? 100 : m?.alignment === '역배열' ? 0 : m?.alignment === '혼조' ? 50 : undefined;
    const ma224Score = m?.ma224 === 'passed' ? 100 : m?.ma224 === 'na' ? 50 : m?.ma224 === 'fail' ? 35 : undefined;
    // 낙폭회복은 "있으면 강한 진입 신호, 없어도 감점 아님" — passed=100 / 그 외 중립 50.
    const dropScore = m?.dropRecovery === true ? 100 : m?.dropRecovery === false ? 50 : undefined;
    // RSI 는 역발상 해석 — 과열은 진입 리스크(25), 침체는 반등 여지(75).
    const rsiScore = m?.rsi === '과열' ? 25 : m?.rsi === '침체' ? 75 : m?.rsi === '중립' ? 60 : undefined;
    return [return12mPct, alignScore, ma224Score, dropScore, rsiScore];
}

/**
 * mini F-Score (Piotroski 축약판 — 보유 데이터로 가능한 4개 항목):
 *   ① ROE > 0  ② 영업이익률 > 0  ③ 성장(매출 or 영업이익 YoY) > 0  ④ 부채비율 ≤ 150%
 * 평가 가능한 항목 2개 이상 & 통과가 절반 미만이면 value trap 위험 → Value 축 50점 캡.
 */
function miniFScore(s: ScoreInputs): { passed: number; available: number } {
    let passed = 0, available = 0;
    const check = (v: number | undefined, ok: (x: number) => boolean) => {
        if (v === undefined || !Number.isFinite(v)) return;
        available++;
        if (ok(v)) passed++;
    };
    check(s.roe, x => x > 0);
    check(s.operatingMargin, x => x > 0);
    const growth = s.opProfitGrowthYoY ?? s.revenueGrowthYoY;
    check(growth, x => x > 0);
    check(s.debtRatio, x => x <= 150);
    return { passed, available };
}

/**
 * 후보 배치 전체를 한 번에 채점한다 — 백분위 랭크가 cross-sectional(후보군 내 상대평가)
 * 이므로 반드시 배치 단위로 호출. 반환 순서는 입력 순서와 동일.
 */
export function scoreCandidates(inputs: ScoreInputs[]): CompositeScore[] {
    // 지표별 백분위 (배치 전체 기준)
    const pRoe = percentileRanks(inputs.map(s => s.roe));
    const pOpm = percentileRanks(inputs.map(s => s.operatingMargin));
    const pGrowth = percentileRanks(inputs.map(s => s.opProfitGrowthYoY ?? s.revenueGrowthYoY));
    const pDebt = percentileRanks(inputs.map(s => s.debtRatio), true);
    const pPer = percentileRanks(inputs.map(s => (s.per !== undefined && s.per > 0 ? s.per : undefined)), true);
    const pPbr = percentileRanks(inputs.map(s => (s.pbr !== undefined && s.pbr > 0 ? s.pbr : undefined)), true);
    const pRet = percentileRanks(inputs.map(s => s.retentionRatio));
    const pCap = percentileRanks(inputs.map(s => s.marketCapEok));
    const pR12 = percentileRanks(inputs.map(s => s.momentum?.return12m));

    return inputs.map((s, i) => {
        const quality = axisMean([pRoe[i], pOpm[i], pGrowth[i], pDebt[i]]);
        let value = axisMean([pPer[i], pPbr[i]]);
        const momentum = axisMean(momentumSubScores(s.momentum, pR12[i]));
        const stability = axisMean([pRet[i], pCap[i], pDebt[i]]);

        // value trap 가드 — 품질 체크 절반 미만 통과면 "싸다" 가산을 중립(50)까지로 제한.
        const f = miniFScore(s);
        let valueTrapGuarded = false;
        if (value !== undefined && f.available >= 2 && f.passed < f.available / 2 && value > 50) {
            value = 50;
            valueTrapGuarded = true;
        }

        // 결측 축 제외하고 가중치 재정규화.
        const base = STYLE_WEIGHTS[s.style || ''] || EQUAL_WEIGHTS;
        const axes: { key: keyof AxisWeights; score: number | undefined }[] = [
            { key: 'q', score: quality }, { key: 'v', score: value },
            { key: 'm', score: momentum }, { key: 's', score: stability },
        ];
        const availAxes = axes.filter(a => a.score !== undefined);
        const wSum = availAxes.reduce((a, b) => a + base[b.key], 0);
        const total = wSum > 0
            ? availAxes.reduce((acc, a) => acc + (a.score as number) * (base[a.key] / wSum), 0)
            : 50; // 전 축 결측 — 중립
        const weights: AxisWeights = { q: 0, v: 0, m: 0, s: 0 };
        for (const a of availAxes) weights[a.key] = base[a.key] / (wSum || 1);

        const fmt = (v: number | undefined) => (v === undefined ? '-' : String(Math.round(v)));
        return {
            symbol: s.symbol,
            total: Math.round(total * 10) / 10,
            quality, value, momentum, stability,
            valueTrapGuarded,
            fScore: f.available >= 2 ? `${f.passed}/${f.available}` : undefined,
            weights,
            breakdown: `Q${fmt(quality)} V${fmt(value)} M${fmt(momentum)} S${fmt(stability)}`,
        };
    });
}
