# scraper2_hj3415/app/parsing/c106_parser.py
from __future__ import annotations

import re
from collections.abc import Mapping, Sequence
from io import StringIO
from typing import Any, Hashable, cast

import numpy as np
import pandas as pd
from common_hj3415.utils import clean_text
from logging_hj3415 import logger
from scraper2_hj3415.app.nfs.domain.types import Records
from scraper2_hj3415.app.nfs.parsing.normalize.label import (
    normalize_metric_label,
    sanitize_label,
)
from scraper2_hj3415.app.nfs.ports.browser.browser_port import BrowserPort

_CODE_RE = re.compile(r"\b\d{6}\b")


async def parse_c106_header_codes(browser: BrowserPort) -> list[str]:
    """
    현재 페이지에서 '기업간비교자료' 헤더(회사명들)에서 종목코드(6자리)만 추출한다.
    (goto/sleep 없음)
    """
    selector = (
        'xpath=//caption[contains(normalize-space(.), "기업간비교자료")]'
        "/following-sibling::thead//th[not(@colspan)]"
    )
    await browser.wait_attached(selector)
    th_texts = await browser.all_texts(selector)

    codes: list[str] = []
    for _, t in enumerate(th_texts):
        text = (t or "").strip()
        if not text:
            continue
        m = _CODE_RE.search(text)
        if not m:
            continue
        codes.append(m.group(0))

    # 중복 제거(순서 유지)
    seen: set[str] = set()
    uniq: list[str] = []
    for c in codes:
        if c not in seen:
            seen.add(c)
            uniq.append(c)
    logger.debug(f"c106 header codes: {uniq}")
    return uniq


def html_table_to_df(html: str, codes: list[str]) -> pd.DataFrame:
    """
    ✅ "코드 없는 컬럼은 드랍" 정책 버전

    전제/문제:
      - c106 테이블은 좌측 2컬럼(그룹/항목) + 우측 N개(회사 컬럼) 구조.
      - 헤더에서 종목코드(6자리)를 추출해 codes로 넘기는데,
        어떤 케이스는 회사 컬럼 수(N) > len(codes) 가 될 수 있음(업종평균/빈 컬럼 등).
      - 이때 df.columns 길이 불일치로 `Length mismatch`가 발생.

    해결:
      - read_html 결과 df의 실제 컬럼 수를 기준으로 회사 컬럼 수(N)를 계산
      - N > len(codes) 이면 "오른쪽 끝"에서 (N-len(codes))개 회사 컬럼을 드랍
        (드랍 대상은 코드가 매칭되지 않는 컬럼 = 코드 없는 컬럼으로 간주)
      - N < len(codes)이면 codes를 좌측부터 N개로 truncate

    반환:
      - NaN은 None으로 정리한 DataFrame
    """
    df = pd.read_html(StringIO(html), header=None)[0]

    if df.empty:
        return pd.DataFrame()

    n_cols = int(df.shape[1])
    if n_cols < 2:
        return pd.DataFrame()

    # 좌측 2개는 (항목_group, 항목) 고정
    n_company = n_cols - 2

    # --- codes 보정(드랍 정책) ---
    codes2 = list(codes)

    drop_idxs = []
    if n_company > len(codes2):
        drop_n = n_company - len(codes2)
        # 오른쪽 끝에서 drop_n개 컬럼 제거 (위치 기반)
        df = df.iloc[:, : n_cols - drop_n]

        # 드랍 후 재계산
        n_cols = int(df.shape[1])
        n_company = n_cols - 2

    elif n_company < len(codes2):
        # df가 더 좁으면 codes도 거기에 맞춰 자름
        codes2 = codes2[:n_company]

    # 이제 길이가 반드시 맞아야 함
    df.columns = ["항목_group", "항목"] + codes2

    # --- 이하: 기존 로직 유지 ---
    df["항목_group"] = df["항목_group"].ffill()

    # 첫 두 줄 주가데이터 주입(기존 로직 유지)
    for i in range(min(2, len(df))):
        row = df.iloc[i].tolist()
        new_row = ["주가데이터"] + row
        df.loc[i] = new_row[: len(df.columns)]

    df = df[df["항목"].notna()].reset_index(drop=True)
    df.loc[df["항목"].isin(["투자의견", "목표주가(원)"]), "항목_group"] = "기타지표"
    df = df[df["항목"] != "재무연월"].reset_index(drop=True)

    for col in df.columns[2:]:
        df[col] = df[col].replace("-", "0")
        df[col] = pd.to_numeric(df[col], errors="coerce")

    # clean_text는 기존 네 코드에서 import해서 쓰는 걸 전제로 함
    # (여기서는 이름만 유지)
    df["항목_group"] = df["항목_group"].astype("string").map(clean_text)  # type: ignore[name-defined]
    df["항목"] = df["항목"].astype("string").map(clean_text)  # type: ignore[name-defined]

    logger.debug(f"raw df.shape={df.shape} raw df.columns={list(df.columns)}")
    logger.debug(
        f"n_cols={n_cols} n_company={n_company} codes={len(codes)} drop_idxs={drop_idxs}"
    )
    return df.replace({np.nan: None})


def df_to_c106_metric_list(
    df: pd.DataFrame,
) -> Sequence[Mapping[Hashable, Any]]:  # pyright: ignore[reportExplicitAny]
    """
    C106 DataFrame -> records(list[dict])

    A안 적용:
    - 항목(key)은 normalize_c1034_item으로 강하게 정규화(괄호/별표 등 제거)
    - 항목_raw는 정규화 전(단 UI 노이즈만 제거된) 원라벨을 저장
    - 항목_group은 그대로 두되, 필요 없으면 caller에서 삭제하면 됨
    """
    if df.empty:
        return []

    df = df.copy()

    # raw 보존(정규화 전, UI 노이즈만 제거)
    raw = df["항목"].where(
        df["항목"].notna(), None  # pyright: ignore[reportArgumentType]
    )
    df["항목_raw"] = raw.map(
        lambda x: (  # pyright: ignore[reportAny]
            sanitize_label(x) if x is not None else None
        )
    )

    # 항목_group 컬럼들은 제거(있을 때만)
    drop_cols = [c for c in ("항목_group", "항목_group_raw") if c in df.columns]
    if drop_cols:
        df = df.drop(columns=drop_cols)

    # key 정규화(A안)
    df["항목"] = df["항목"].map(
        lambda x: normalize_metric_label(  # pyright: ignore[reportAny]
            None if x is None else str(x)  # pyright: ignore[reportAny]
        )
    )

    # 유효 행만
    df = df[df["항목"].astype(str).str.strip() != ""].reset_index(drop=True)

    # NaN -> None
    df = df.where(pd.notnull(df), None)  # pyright: ignore[reportArgumentType]

    return df.to_dict(orient="records")


async def parse_c106_current_table(
    browser: BrowserPort,
    *,
    columns: list[str],
    table_selector: str = "#cTB611",
    table_index: int = 0,
    timeout_ms: int = 10_000,
) -> Records:
    """
    ✅ 현재 화면(이미 goto/대기 완료된 상태)에서 비교테이블만 파싱한다.
    """
    await browser.wait_table_nth_ready(
        table_selector, index=table_index, min_rows=3, timeout_ms=timeout_ms
    )
    html = await browser.outer_html_nth(table_selector, table_index)
    df = html_table_to_df(html, columns)
    return cast(Records, df_to_c106_metric_list(df))
