# -*- coding: utf-8 -*-
"""
行情与参考位取数 (best-effort, 取不到一律返回 None 由上层降级)
==============================================================
现价: Redis db13 (SIGNAL_REDIS_DB_QUOTES), key `tushare:rt_min:1MIN:{600000.SH}`,
      值为当日分钟 K 线数组 —— 与 bionic_trader 既有读法完全一致, 不另立口径。
参考位: 主口径取决策系统 strategy_daily_results 的支撑/压力; 日龄超 PMS_REF_STALE_TDAYS
      转兜底自算 (设计 §13): 支撑 = max(MA20, 近20日低点×1.01), 压力 = 近60日高点,
      止损 = 底仓成本 − N×ATR 与支撑取高。因子分表 gp_stock_factor_pro_YYYYMM 经 153 代理
      逐表单查 (跨分表 UNION 会被代理拒绝, 沿用 bionic 的踩坑结论)。
"""
from __future__ import annotations

import json
import logging
import threading
from datetime import datetime, timedelta

from config.settings import settings
from app.db.session import fetch_all
from app.repo import downstream_repo
from app.services import param_store

logger = logging.getLogger("pms.market")

_redis = None
_lock = threading.Lock()
QUOTE_KEY = "tushare:rt_min:1MIN:{code}"


def _r():
    """行情 Redis 客户端。

    强制 RESP2 (protocol=2): 目标 Redis 是 6.0 以前的版本, 不认 RESP3 的 HELLO 命令,
    而 redis-py 6.x 默认按 RESP3 握手, 会直接报 "unknown command `HELLO`" 全盘取不到价。
    requirements.txt 已把 redis 锁在 5.x, 这里再显式声明一道 (老版本 redis-py 不认这个
    参数, 用 TypeError 兜回去)。
    """
    global _redis
    if _redis is not None:
        return _redis
    with _lock:
        if _redis is None:
            import redis
            kw = dict(host=settings.SIGNAL_REDIS_HOST, port=settings.SIGNAL_REDIS_PORT,
                      password=settings.SIGNAL_REDIS_PASSWORD or None,
                      db=settings.SIGNAL_REDIS_DB_QUOTES, decode_responses=True,
                      socket_timeout=settings.SIGNAL_REDIS_SOCKET_TIMEOUT)
            try:
                _redis = redis.Redis(protocol=2, **kw)
            except TypeError:
                _redis = redis.Redis(**kw)
    return _redis


def _bars(code: str):
    try:
        raw = _r().get(QUOTE_KEY.format(code=code))
        if not raw:
            return None
        bars = json.loads(raw)
        return bars if isinstance(bars, list) and bars else None
    except Exception as e:
        logger.warning("实时行情读取失败 [%s]: %s", code, e)
        return None


def get_price(ts_code: str):
    bars = _bars(ts_code)
    if not bars:
        return None
    try:
        px = float(bars[-1].get("close") or 0)
        return px if px > 0 else None
    except (TypeError, ValueError):
        return None


def get_prices(codes) -> dict:
    """批量取现价 (逐 key GET; 数量级为持仓数, 无需 pipeline 复杂化)。"""
    out = {}
    for c in codes or []:
        out[c] = get_price(c)
    return out


def day_snapshot(ts_code: str) -> dict:
    """当日分钟线聚合: 现价/开盘/最高/最低/均价(VWAP近似)/涨幅 —— 择时与不追高检查用。"""
    bars = _bars(ts_code)
    if not bars:
        return {}
    try:
        closes = [float(b.get("close") or 0) for b in bars if float(b.get("close") or 0) > 0]
        if not closes:
            return {}
        highs = [float(b.get("high") or b.get("close") or 0) for b in bars]
        lows = [float(b.get("low") or b.get("close") or 0) for b in bars if
                float(b.get("low") or b.get("close") or 0) > 0]
        opens = float(bars[0].get("open") or closes[0])
        vols = [float(b.get("vol") or 0) for b in bars]
        amt = sum(c * v for c, v in zip(closes, vols))
        vwap = (amt / sum(vols)) if sum(vols) > 0 else sum(closes) / len(closes)
        px = closes[-1]
        return {"price": px, "open": opens, "high": max(highs) if highs else px,
                "low": min(lows) if lows else px, "vwap": round(vwap, 3),
                "day_chg_from_open": (px / opens - 1) if opens else None,
                "bar_time": bars[-1].get("time"), "bars": len(bars)}
    except (TypeError, ValueError) as e:
        logger.warning("行情聚合失败 [%s]: %s", ts_code, e)
        return {}


# ================================================================ 参考位
def _shard_tables(months: int = 4) -> list:
    now = datetime.now()
    out, d = [], now
    for _ in range(max(1, months)):
        out.append(f"gp_stock_factor_pro_{d.strftime('%Y%m')}")
        d = (d.replace(day=1) - timedelta(days=1))
    return out


def _factor_rows(ts_code: str, days: int = 70) -> list:
    """逐分表单查 (代理要求单表), 合并后取最近 days 条。symbol 先点式后前缀式试。"""
    since = (datetime.now() - timedelta(days=int(days * 1.6))).strftime("%Y-%m-%d")
    rows = []
    for tbl in _shard_tables():
        for sym in (ts_code, downstream_repo.to_prefix(ts_code)):
            try:
                part = fetch_all(
                    f"SELECT trade_date, close_qfq, high_qfq, low_qfq, atr_qfq FROM {tbl} "
                    f"WHERE symbol = :sym AND trade_date >= :since", {"sym": sym, "since": since})
            except Exception:
                part = []
            if part:
                rows.extend(part)
                break
    rows = [r for r in rows if r.get("close_qfq")]
    rows.sort(key=lambda r: str(r.get("trade_date")))
    return rows[-days:]


def self_calc_refs(ts_code: str, base_cost=None) -> dict:
    """兜底自算参考位 (设计 §13)。数据不足返回 {}。"""
    rows = _factor_rows(ts_code)
    if len(rows) < 20:
        return {}
    close = [float(r["close_qfq"]) for r in rows]
    low = [float(r.get("low_qfq") or r["close_qfq"]) for r in rows]
    high = [float(r.get("high_qfq") or r["close_qfq"]) for r in rows]
    atr = None
    for r in reversed(rows):
        if r.get("atr_qfq"):
            atr = float(r["atr_qfq"])
            break
    ma20 = sum(close[-20:]) / 20
    support = max(ma20, min(low[-20:]) * 1.01)
    pressure = max(high[-60:]) if len(high) >= 60 else max(high)
    stop = None
    if base_cost and atr:
        stop = max(float(base_cost) - param_store.get_float("PMS_STOP_ATR_MULT", 2.0) * atr,
                   support)
    elif atr:
        stop = max(close[-1] - param_store.get_float("PMS_STOP_ATR_MULT", 2.0) * atr, support)
    return {"support": round(support, 3), "pressure": round(pressure, 3),
            "stop": round(stop, 3) if stop else None, "atr": atr, "ma20": round(ma20, 3),
            "source": "self_calc", "bars": len(rows)}


_ma_cache = {"day": None, "data": {}}


def get_last_close(ts_code: str):
    """上一交易日收盘 (日线 close_qfq)。**盘前定价的兜底** —— 按日缓存。

    db13 里是**当日**分钟线, 盘前那张 key 根本不存在, get_price 必然返回 None。
    2026-07-31 盘前实测: 上游计划前 30 只全部"无价", 候选池会整个空掉。计划价只用来
    算批次数量 (真正的委托价由择时环节按实时行情现定), 用昨收完全够用。
    """
    today = datetime.now().strftime("%Y%m%d")
    key = f"PC:{ts_code}"
    if _ma_cache["day"] != today:
        _ma_cache.update({"day": today, "data": {}})
    if key in _ma_cache["data"]:
        return _ma_cache["data"][key]
    val = None
    try:
        rows = _factor_rows(ts_code, days=3)
        closes = [float(r["close_qfq"]) for r in rows if r.get("close_qfq")]
        if closes:
            val = round(closes[-1], 3)
    except Exception as e:
        logger.warning("昨收取数失败 [%s]: %s", ts_code, e)
    _ma_cache["data"][key] = val
    return val


def plan_price(ts_code: str) -> dict:
    """规划用价: 实时价优先, 盘前/停更回落昨收。返回 {price, source}。

    source: realtime / prev_close / none —— **一定要带出来**。拿昨收当现价去做"不追高"
    这类判断会出错, 所以只给规划期定量用, 判断类的检查仍走 day_snapshot。
    """
    px = get_price(ts_code)
    if px:
        return {"price": px, "source": "realtime"}
    px = get_last_close(ts_code)
    if px:
        return {"price": px, "source": "prev_close"}
    return {"price": None, "source": "none"}


def get_ma5(ts_code: str):
    """MA5 (规则闸「不追高」用)。按日缓存 —— 因子分表是日频数据, 盘中重复查没意义。"""
    today = datetime.now().strftime("%Y%m%d")
    if _ma_cache["day"] != today:
        _ma_cache.update({"day": today, "data": {}})
    if ts_code in _ma_cache["data"]:
        return _ma_cache["data"][ts_code]
    val = None
    try:
        rows = _factor_rows(ts_code, days=10)
        closes = [float(r["close_qfq"]) for r in rows if r.get("close_qfq")]
        if len(closes) >= 5:
            val = round(sum(closes[-5:]) / 5, 3)
    except Exception as e:
        logger.warning("MA5 取数失败 [%s]: %s", ts_code, e)
    _ma_cache["data"][ts_code] = val
    return val


def get_high5(ts_code: str):
    """近 5 个交易日最高价 (动作引擎「创 5 日新高」用)。同样按日缓存。"""
    today = datetime.now().strftime("%Y%m%d")
    key = f"H5:{ts_code}"
    if _ma_cache["day"] != today:
        _ma_cache.update({"day": today, "data": {}})
    if key in _ma_cache["data"]:
        return _ma_cache["data"][key]
    val = None
    try:
        rows = _factor_rows(ts_code, days=10)
        highs = [float(r.get("high_qfq") or r.get("close_qfq") or 0) for r in rows]
        highs = [h for h in highs if h > 0]
        if len(highs) >= 5:
            val = round(max(highs[-5:]), 3)
    except Exception as e:
        logger.warning("5日高点取数失败 [%s]: %s", ts_code, e)
    _ma_cache["data"][key] = val
    return val


def get_refs(ts_code: str, *, base_cost=None) -> dict:
    """参考位: 决策系统主口径 → 日龄超期/缺失时兜底自算 → 都拿不到返回 source=none。"""
    stale_days = param_store.get_int("PMS_REF_STALE_TDAYS", 3)
    try:
        r = downstream_repo.fetch_refs(ts_code)
    except Exception as e:
        logger.warning("读决策系统结论失败 [%s]: %s", ts_code, e)
        r = None
    if r and (r.get("support") or r.get("pressure")):
        age = _age_days(r.get("trade_date"))
        if age is not None and age <= stale_days * 2:      # 自然日宽松换算交易日
            return {"support": r.get("support"), "pressure": r.get("pressure"),
                    "stop": r.get("support"), "source": "bionic",
                    "trade_date": str(r.get("trade_date")), "age_days": age}
    try:
        s = self_calc_refs(ts_code, base_cost=base_cost)
    except Exception as e:
        logger.warning("自算参考位失败 [%s]: %s", ts_code, e)
        s = {}
    if s:
        s["note"] = "兜底口径 (决策系统结论缺失或停更)"
        return s
    return {"support": None, "pressure": None, "stop": None, "source": "none",
            "note": "参考位不可用 —— 敞口无法计算, 相关动作按保守处理"}


def _age_days(trade_date):
    if not trade_date:
        return None
    try:
        s = str(trade_date)[:10].replace("-", "")
        d = datetime.strptime(s, "%Y%m%d").date()
        return (datetime.now().date() - d).days
    except (ValueError, TypeError):
        return None
