from __future__ import annotations

import json
from typing import Any

from backend.data.contracts import ProviderResult
from backend.data.repository import MarketRepository
from backend.database.connection import Database


def assemble_screener_inputs(
    database: Database,
    repository: MarketRepository,
    trade_date: str,
    trade_dates: tuple[str, ...],
    raw: dict[str, ProviderResult | None],
) -> tuple[dict[str, Any], dict[str, float]]:
    inputs = {
        key: [_normalize_row(row) for row in value.rows]
        for key, value in raw.items()
        if isinstance(value, ProviderResult)
    }
    inputs["earnings"] = _earnings_events(
        inputs.pop("forecast", []), inputs.pop("express", []), trade_date
    )
    popularity, popularity_coverage = _popularity(database, repository, trade_date)
    institutions, institution_coverage = _institutions(database, repository, trade_date)
    local_limits, local_limit_coverage = _local_limit_events(
        database, repository, trade_dates[-80:]
    )
    inputs["popularity"] = popularity
    inputs["institutions"] = institutions
    inputs["limit_events"] = _merge_events(inputs.get("limit_events", []), local_limits)

    directory_count = len(inputs.get("directory", []))
    daily_current = _on_date(inputs.get("daily", []), trade_date)
    current_count = len({str(row.get("ts_code") or "") for row in daily_current})
    expected = max(directory_count, current_count, 1)
    basic_current = _on_date(inputs.get("daily_basic", []), trade_date)
    fundamentals = _latest_announced(inputs.get("fundamentals", []), trade_date)
    moneyflow_counts = _code_date_counts(inputs.get("moneyflow", []))
    industry_codes = {
        str(row.get("ts_code") or "")
        for row in inputs.get("industry", [])
        if _active_member(row, trade_date)
    }
    auction_count = len({str(row.get("ts_code") or "") for row in inputs.get("auction", [])})
    limit_result = raw.get("limit_events")
    provider_limit_coverage = (
        limit_result.metadata.coverage if isinstance(limit_result, ProviderResult) else 0
    )
    forecast = raw.get("forecast")
    express = raw.get("express")
    earnings_coverage = min(
        forecast.metadata.coverage if isinstance(forecast, ProviderResult) else 0,
        express.metadata.coverage if isinstance(express, ProviderResult) else 0,
    )
    coverage = {
        "market": min(current_count / expected, 1),
        "valuation": min(
            len({str(row.get("ts_code") or "") for row in basic_current}) / expected, 1
        ),
        "financial": min(len(fundamentals) / expected, 1),
        "moneyflow": min(
            sum(count >= min(5, len(trade_dates)) for count in moneyflow_counts.values())
            / expected,
            1,
        ),
        "industry": min(len(industry_codes) / expected, 1),
        "auction": min(auction_count / expected, 1),
        "popularity": popularity_coverage,
        "institutions": institution_coverage,
        "earnings": earnings_coverage,
        "limit_events": max(provider_limit_coverage, local_limit_coverage),
    }
    return inputs, coverage


def _popularity(
    database: Database, repository: MarketRepository, trade_date: str
) -> tuple[list[dict[str, Any]], float]:
    with database.read() as connection:
        row = repository.insight_snapshot(connection, "popularity", trade_date)
    if row is None:
        return [], 0.0
    payload = json.loads(str(row["payload_json"]))
    return [
        {
            "ts_code": str(item.get("identifier") or ""),
            "combined_score": item.get("score"),
            "rank_change": item.get("rank_change"),
            "dual_source": bool(item.get("dual_source")),
        }
        for item in payload.get("combined") or []
        if item.get("identifier")
    ], float(row["coverage"])


def _institutions(
    database: Database, repository: MarketRepository, trade_date: str
) -> tuple[list[dict[str, Any]], float]:
    with database.read() as connection:
        row = repository.insight_snapshot(connection, "dragon-list", trade_date)
    if row is None:
        return [], 0.0
    payload = json.loads(str(row["payload_json"]))
    seats = payload.get("seats")
    if seats is None:
        return [], 0.0
    grouped: dict[str, dict[str, float]] = {}
    for item in seats:
        identifier = str(item.get("ts_code") or "")
        if not identifier or "机构" not in str(item.get("exalter") or ""):
            continue
        target = grouped.setdefault(identifier, {"net": 0.0, "count": 0})
        target["net"] += float(item.get("net_buy") or 0) / 1_000_000
        target["count"] += 1
    return [
        {
            "ts_code": identifier,
            "net_buy_million": values["net"],
            "seat_count": int(values["count"]),
        }
        for identifier, values in grouped.items()
    ], float(row["coverage"])


def _local_limit_events(
    database: Database,
    repository: MarketRepository,
    trade_dates: tuple[str, ...],
) -> tuple[list[dict[str, Any]], float]:
    if not trade_dates:
        return [], 0.0
    with database.read() as connection:
        rows = repository.summaries(connection, trade_dates[-1], len(trade_dates))
    expected = set(trade_dates)
    covered: set[str] = set()
    events = []
    for row in rows:
        trade_date = str(row["trade_date"])
        if trade_date not in expected:
            continue
        covered.add(trade_date)
        payload = json.loads(str(row["payload_json"]))
        for key, event in (("limits", "U"), ("broken", "Z"), ("down_limits", "D")):
            events.extend(
                {
                    "trade_date": trade_date,
                    "ts_code": str(item["identifier"]),
                    "limit_type": event,
                }
                for item in payload.get(key) or []
                if item.get("identifier")
            )
    return events, len(covered) / len(expected)


def _normalize_row(row: dict[str, Any]) -> dict[str, Any]:
    result = dict(row)
    for field in ("trade_date", "ann_date", "end_date", "ex_date", "in_date", "out_date"):
        value = str(result.get(field) or "")
        if len(value) == 8 and value.isdigit():
            result[field] = f"{value[:4]}-{value[4:6]}-{value[6:]}"
    if result.get("price") is not None and result.get("pre_close") is not None:
        price = _number(result.get("price"))
        previous = _number(result.get("pre_close"))
        result["change"] = (price / previous - 1) * 100 if price is not None and previous else None
        amount = _number(result.get("amount"))
        result["amount_million"] = amount / 1_000_000 if amount is not None else None
    return result


def _earnings_events(
    forecasts: list[dict[str, Any]], expresses: list[dict[str, Any]], through: str
) -> list[dict[str, Any]]:
    forecast_map: dict[tuple[str, str], dict[str, Any]] = {}
    for row in forecasts:
        key = (str(row.get("ts_code") or ""), str(row.get("end_date") or ""))
        announced = str(row.get("ann_date") or "")
        current = forecast_map.get(key)
        if (
            all(key)
            and announced
            and announced <= through
            and (current is None or announced > str(current.get("ann_date") or ""))
        ):
            forecast_map[key] = row
    result = []
    for row in expresses:
        key = (str(row.get("ts_code") or ""), str(row.get("end_date") or ""))
        announced = str(row.get("ann_date") or "")
        forecast = forecast_map.get(key)
        if forecast is None or not announced or announced > through:
            continue
        values = [
            value
            for value in (
                _number(forecast.get("net_profit_min")),
                _number(forecast.get("net_profit_max")),
            )
            if value is not None
        ]
        expected = sum(values) / len(values) if values else None
        actual = _number(row.get("n_income"))
        if expected in (None, 0) or actual is None:
            continue
        if abs(actual) > max(abs(expected), 1) * 100:
            actual /= 10000
        result.append(
            {
                "ts_code": key[0],
                "end_date": key[1],
                "ann_date": announced,
                "surprise_pct": (actual / expected - 1) * 100,
            }
        )
    return result


def _on_date(rows: list[dict[str, Any]], trade_date: str) -> list[dict[str, Any]]:
    return [row for row in rows if str(row.get("trade_date") or "") == trade_date]


def _latest_announced(rows: list[dict[str, Any]], through: str) -> dict[str, dict[str, Any]]:
    result: dict[str, dict[str, Any]] = {}
    for row in rows:
        identifier = str(row.get("ts_code") or "")
        announced = str(row.get("ann_date") or "")
        current = result.get(identifier)
        if (
            identifier
            and announced
            and announced <= through
            and (current is None or announced > str(current.get("ann_date") or ""))
        ):
            result[identifier] = row
    return result


def _code_date_counts(rows: list[dict[str, Any]]) -> dict[str, int]:
    values: dict[str, set[str]] = {}
    for row in rows:
        identifier = str(row.get("ts_code") or "")
        if identifier:
            values.setdefault(identifier, set()).add(str(row.get("trade_date") or ""))
    return {identifier: len(dates) for identifier, dates in values.items()}


def _active_member(row: dict[str, Any], trade_date: str) -> bool:
    start = str(row.get("in_date") or "")
    end = str(row.get("out_date") or "")
    return (not start or start <= trade_date) and (not end or end > trade_date)


def _merge_events(
    provider: list[dict[str, Any]], local: list[dict[str, Any]]
) -> list[dict[str, Any]]:
    merged = {
        (str(row.get("trade_date") or ""), str(row.get("ts_code") or "")): row for row in local
    }
    for row in provider:
        merged[(str(row.get("trade_date") or ""), str(row.get("ts_code") or ""))] = row
    return list(merged.values())


def _number(value: Any) -> float | None:
    try:
        result = float(value)
        return result if result == result else None
    except (TypeError, ValueError):
        return None
