from __future__ import annotations

from dataclasses import dataclass
from datetime import date
import json
from pathlib import Path
import struct

import numpy as np
import pandas as pd

from .config import Settings
from .tdx import read_day_dates


HEADER = struct.Struct("<hIH3I")
INDEX_RECORD = struct.Struct("<6scI")
FINANCIAL_SCHEMA_VERSION = 1

# The numeric indexes follow the local TDX professional-finance dictionary.
FINANCIAL_FIELDS: dict[int, str] = {
    1: "basic_eps", 2: "deducted_eps", 4: "net_assets_per_share", 6: "roe_reported",
    7: "operating_cash_flow_per_share", 8: "money_funds", 10: "bills_receivable",
    11: "accounts_receivable", 12: "prepayments", 13: "other_receivables", 17: "inventory",
    21: "current_assets", 25: "long_term_equity_investment", 27: "fixed_assets",
    28: "construction_in_progress", 33: "intangible_assets", 34: "development_expenditure",
    35: "goodwill", 40: "total_assets", 41: "short_term_borrowings", 43: "bills_payable",
    44: "accounts_payable", 52: "noncurrent_liabilities_due_1y", 54: "current_liabilities",
    55: "long_term_borrowings", 56: "bonds_payable", 57: "long_term_payables",
    59: "provisions", 62: "noncurrent_liabilities", 63: "total_liabilities",
    64: "registered_capital", 69: "minority_equity", 72: "total_equity",
    74: "operating_revenue_ytd", 75: "operating_cost_ytd", 77: "selling_expense_ytd",
    78: "management_expense_ytd", 80: "finance_expense_ytd", 81: "impairment_loss_ytd",
    82: "fair_value_change_ytd", 83: "investment_income_ytd", 86: "operating_profit_ytd",
    87: "subsidy_income_ytd", 88: "non_operating_income_ytd", 89: "non_operating_expense_ytd",
    92: "total_profit_ytd", 93: "income_tax_ytd", 95: "net_profit_ytd",
    96: "parent_net_profit_ytd", 97: "minority_profit_ytd", 98: "cash_received_sales_ytd",
    107: "operating_cash_flow_ytd", 114: "capital_expenditure_cash_ytd",
    121: "borrowings_cash_received_ytd", 124: "debt_repayment_cash_ytd",
    125: "dividends_interest_cash_ytd", 128: "financing_cash_flow_ytd",
    131: "net_cash_increase_ytd", 159: "current_ratio_reported", 160: "quick_ratio_reported",
    162: "interest_coverage_reported", 172: "receivables_turnover_reported",
    173: "inventory_turnover_reported", 175: "asset_turnover_reported",
    183: "operating_revenue_yoy_reported", 184: "net_profit_yoy_reported",
    197: "roe_alt_reported", 199: "net_margin_reported", 202: "gross_margin_reported",
    206: "deducted_net_profit_ytd", 207: "ebit_ytd", 208: "ebitda_ytd",
    210: "debt_ratio_reported", 220: "cash_revenue_ratio_reported",
    228: "ocf_net_profit_ratio_reported", 230: "operating_revenue_single_quarter",
    232: "parent_net_profit_single_quarter", 233: "deducted_net_profit_single_quarter",
    234: "operating_cash_flow_single_quarter", 238: "total_shares", 239: "listed_a_shares",
    242: "shareholder_count", 266: "free_float_shares", 271: "parent_equity",
    276: "net_profit_recent_year", 281: "weighted_roe_reported",
    283: "operating_revenue_recent_year", 304: "research_development_expense_ytd",
    305: "interest_expense_ytd", 307: "operating_cash_flow_ttm",
    308: "parent_net_profit_ttm", 309: "deducted_net_profit_ttm",
    311: "basic_eps_single_quarter", 312: "total_operating_revenue_single_quarter",
    319: "total_operating_revenue_ttm", 320: "employee_count",
    321: "fcff_per_share", 322: "fcfe_per_share",
    295: "notes_and_accounts_payable", 296: "notes_and_accounts_receivable",
    297: "deferred_income", 299: "other_equity_instruments", 300: "other_income_ytd",
    301: "asset_disposal_income_ytd", 302: "continuing_operations_net_profit_ytd",
    303: "discontinued_operations_net_profit_ytd",
}


@dataclass(frozen=True)
class FinanceBuildResult:
    rebuilt: bool
    rows: int
    stocks: int
    output_path: Path


def _parse_announcement(value: float) -> pd.Timestamp | None:
    if not np.isfinite(value) or value <= 0:
        return None
    raw = int(value)
    if raw < 1_000_000:
        raw += 20_000_000
    parsed = pd.to_datetime(str(raw), format="%Y%m%d", errors="coerce")
    return None if pd.isna(parsed) else parsed


def _next_trade_dates(announcements: pd.Series, calendar: np.ndarray) -> pd.Series:
    ann = announcements.to_numpy(dtype="datetime64[D]")
    positions = np.searchsorted(calendar, ann, side="right")
    values = np.full(len(positions), np.datetime64("NaT"), dtype="datetime64[D]")
    valid = positions < len(calendar)
    values[valid] = calendar[positions[valid]]
    return pd.Series(pd.to_datetime(values), index=announcements.index)


def extract_financial_pit(settings: Settings, force: bool = False) -> FinanceBuildResult:
    source_dir = settings.tdx_root / "vipdoc" / "cw"
    files = [path for path in sorted(source_dir.glob("gpcw*.dat"))
             if path.stem[4:] >= "20181231" and path.stat().st_size > HEADER.size]
    if not files:
        raise RuntimeError(f"没有找到可用专业财务文件：{source_dir}")
    target = settings.financial_pit_path
    newest_source = max(path.stat().st_mtime for path in files)
    metadata_path = settings.report_dir / "financial_metadata.json"
    if target.exists() and target.stat().st_mtime >= newest_source and metadata_path.exists() and not force:
        cached_metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
        if cached_metadata.get("schema_version") == FINANCIAL_SCHEMA_VERSION:
            cached = pd.read_parquet(target, columns=["source_code"])
            return FinanceBuildResult(False, len(cached), cached["source_code"].nunique(), target)

    rows: list[dict[str, object]] = []
    file_audit: list[dict[str, object]] = []
    max_field = max(max(FINANCIAL_FIELDS), 314)
    for path in files:
        raw = path.read_bytes()
        file_type, report_period_raw, record_count, _, record_size, _ = HEADER.unpack_from(raw)
        field_count = record_size // 4 if record_size % 4 == 0 else 0
        accepted = 0
        invalid_announcement = 0
        invalid_offset = 0
        for index in range(record_count):
            code_raw, _, offset = INDEX_RECORD.unpack_from(raw, HEADER.size + index * INDEX_RECORD.size)
            code = code_raw.decode("ascii", errors="ignore")
            if not code.startswith(("0", "3", "6")):
                continue
            if field_count < max_field or offset + record_size > len(raw):
                invalid_offset += 1
                continue
            values = np.frombuffer(raw, dtype="<f4", count=field_count, offset=offset).astype(float)
            announcement = _parse_announcement(values[313])
            report_period = pd.to_datetime(str(report_period_raw), format="%Y%m%d", errors="coerce")
            if announcement is None or pd.isna(report_period) or announcement <= report_period:
                invalid_announcement += 1
                continue
            record: dict[str, object] = {
                "source_code": ("sh" if code.startswith("6") else "sz") + code,
                "report_period": report_period,
                "announcement_date": announcement,
                "source_file": path.name,
            }
            for field_id, field_name in FINANCIAL_FIELDS.items():
                value = values[field_id - 1]
                record[field_name] = value if np.isfinite(value) and abs(value) < 1e30 else np.nan
            rows.append(record)
            accepted += 1
        file_audit.append({
            "source_file": path.name,
            "report_period": report_period_raw,
            "file_type": file_type,
            "file_size": len(raw),
            "record_count": record_count,
            "record_size": record_size,
            "field_count": field_count,
            "accepted_records": accepted,
            "invalid_announcement_records": invalid_announcement,
            "invalid_offset_records": invalid_offset,
        })

    frame = pd.DataFrame(rows).sort_values(["source_code", "announcement_date", "report_period"])
    frame = frame.drop_duplicates(["source_code", "report_period"], keep="last")
    monotonic = frame["report_period"].ge(frame.groupby("source_code")["report_period"].cummax())
    frame = frame.loc[monotonic].copy()
    calendar = read_day_dates(settings.tdx_root / "vipdoc" / "sh" / "lday" / "sh000001.day")
    frame["available_trade_date"] = _next_trade_dates(frame["announcement_date"], calendar)
    frame["period_type"] = frame["report_period"].dt.month.map({3: "Q1", 6: "H1", 9: "Q3", 12: "FY"})
    frame["fiscal_year"] = frame["report_period"].dt.year
    frame["is_vintage_snapshot"] = False
    frame = frame.sort_values(["source_code", "available_trade_date", "report_period"]).reset_index(drop=True)

    target.parent.mkdir(parents=True, exist_ok=True)
    settings.report_dir.mkdir(parents=True, exist_ok=True)
    frame.to_parquet(target, index=False)
    latest = frame.sort_values(["source_code", "report_period", "announcement_date"]).drop_duplicates("source_code", keep="last")
    latest.to_parquet(settings.processed_dir / "financial_latest.parquet", index=False)

    key_columns = {"source_code", "report_period", "announcement_date", "available_trade_date",
                   "source_file", "period_type", "fiscal_year", "is_vintage_snapshot"}
    quality_rows = []
    for column in frame.columns:
        if column in key_columns:
            continue
        series = pd.to_numeric(frame[column], errors="coerce")
        quality_rows.append({
            "field": column,
            "non_null_count": int(series.notna().sum()),
            "non_null_rate": float(series.notna().mean()),
            "zero_rate_among_non_null": float(series.dropna().eq(0).mean()) if series.notna().any() else None,
            "min": float(series.min()) if series.notna().any() else None,
            "median": float(series.median()) if series.notna().any() else None,
            "max": float(series.max()) if series.notna().any() else None,
        })
    quality = pd.DataFrame(quality_rows).sort_values(["non_null_rate", "field"], ascending=[False, True])
    quality.to_csv(settings.processed_dir / "financial_field_quality.csv", index=False, encoding="utf-8-sig")
    pd.DataFrame(file_audit).to_csv(settings.processed_dir / "financial_file_audit.csv", index=False, encoding="utf-8-sig")

    validation = _validate_financial_relations(frame)
    metadata = {
        "schema_version": FINANCIAL_SCHEMA_VERSION,
        "build_date": date.today().isoformat(),
        "rows": int(len(frame)),
        "stocks": int(frame["source_code"].nunique()),
        "first_report_period": frame["report_period"].min().date().isoformat(),
        "latest_report_period": frame["report_period"].max().date().isoformat(),
        "latest_announcement_date": frame["announcement_date"].max().date().isoformat(),
        "latest_available_trade_date": frame["available_trade_date"].max().date().isoformat(),
        "selected_financial_fields": len(FINANCIAL_FIELDS),
        "vintage_snapshot": False,
        "restatement_warning": "本地文件只保留当前下载版本，历史重述无法还原。",
        "validation": validation,
        "all_zero_fields": quality.loc[quality["zero_rate_among_non_null"].eq(1), "field"].tolist(),
    }
    (settings.report_dir / "financial_metadata.json").write_text(
        json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    _write_financial_report(settings, metadata, quality)
    return FinanceBuildResult(True, len(frame), frame["source_code"].nunique(), target)


def _relation_error(frame: pd.DataFrame, calculated: pd.Series, reported: str) -> dict[str, float | int | None]:
    actual = pd.to_numeric(frame[reported], errors="coerce")
    mask = calculated.notna() & actual.notna() & np.isfinite(calculated) & np.isfinite(actual)
    error = (calculated[mask] - actual[mask]).abs()
    return {
        "samples": int(mask.sum()),
        "median_absolute_error": float(error.median()) if len(error) else None,
        "p95_absolute_error": float(error.quantile(0.95)) if len(error) else None,
    }


def _validate_financial_relations(frame: pd.DataFrame) -> dict[str, object]:
    assets = frame["total_assets"].replace(0, np.nan)
    current_liabilities = frame["current_liabilities"].replace(0, np.nan)
    revenue = frame["operating_revenue_ytd"].replace(0, np.nan)
    debt_ratio = frame["total_liabilities"] / assets * 100
    current_ratio = frame["current_assets"] / current_liabilities
    gross_margin = (frame["operating_revenue_ytd"] - frame["operating_cost_ytd"]) / revenue * 100
    return {
        "debt_ratio_pct_points": _relation_error(frame, debt_ratio, "debt_ratio_reported"),
        "current_ratio": _relation_error(frame, current_ratio, "current_ratio_reported"),
        "gross_margin_pct_points": _relation_error(frame, gross_margin, "gross_margin_reported"),
    }


def _write_financial_report(settings: Settings, metadata: dict[str, object], quality: pd.DataFrame) -> None:
    high_zero = quality.nlargest(15, "zero_rate_among_non_null")
    zero_lines = "\n".join(
        f"| {row.field} | {row.zero_rate_among_non_null:.1%} | {int(row.non_null_count)} |"
        for row in high_zero.itertuples(index=False)
    )
    validation_lines = "\n".join(
        f"| {name} | {item['samples']} | {item['median_absolute_error']:.6g} | {item['p95_absolute_error']:.6g} |"
        for name, item in metadata["validation"].items()
    )
    report = f"""# M1 财务字段质量报告

生成日期：{metadata['build_date']}  
记录数：{metadata['rows']}  
股票数：{metadata['stocks']}  
报告期：{metadata['first_report_period']} 至 {metadata['latest_report_period']}  
最新公告：{metadata['latest_announcement_date']}  
最新可用交易日：{metadata['latest_available_trade_date']}  
抽取财务字段：{metadata['selected_financial_fields']} 个

## 1. 重要时间口径

- 财务数据从公告日之后的首个交易日开始可用。
- `report_period`、`announcement_date` 和 `available_trade_date` 分开保存。
- 本地专业财务文件不是历史版本快照，无法还原后续重述前的原始值；`is_vintage_snapshot` 固定为否。

## 2. 关系式抽样验证

| 关系 | 样本数 | 绝对误差中位数 | 绝对误差P95 |
|---|---:|---:|---:|
{validation_lines}

关系式误差用于检查字段映射和单位，不表示报表本身存在问题。部分金融企业口径不同，后续将分行业复核。

## 3. 原始零值率最高字段

| 字段 | 零值率 | 原始非空数 |
|---|---:|---:|
{zero_lines}

通达信专业财务文件中的数值槽位通常都有值，但部分未披露项目也可能编码为 0。因此“原始非空率 100%”不等于字段真实完整；后续计算指标时需要按字段语义区分“真实零值”和“疑似缺失”。全零字段暂不参与评分。完整统计见 `data/processed/financial_field_quality.csv`。
"""
    settings.report_dir.mkdir(parents=True, exist_ok=True)
    (settings.report_dir / "M1_财务字段质量报告.md").write_text(report, encoding="utf-8")
