# -*- coding: utf-8 -*-
# 卡模式 AppEn 客户端：.ic / .in 接口，须先 bootstrap 再调用业务。

from __future__ import annotations

import os
import sys
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Optional
from urllib.parse import quote, unquote

import requests

from bsphp_crypto import (
    aes128_cbc_encrypt_base64,
    decrypt_response_body,
    md5_hex,
    rsa_encrypt_pkcs1_base64,
)
from bsphp_encode import encode_parameter


@dataclass
class ClientConfig:
    url: str
    mutual_key: str
    server_private_key: str
    client_public_key: str


@dataclass
class APIResult:
    data: Any
    code: Optional[int]

    def message(self) -> str:
        if isinstance(self.data, str):
            return self.data
        if isinstance(self.data, list):
            return "\n".join(str(x) for x in self.data)
        return ""


class InitFailedError(Exception):
    def __init__(self, message: str, *, debug_log: str = "") -> None:
        super().__init__(message)
        self.debug_log = debug_log

    def __str__(self) -> str:
        if self.debug_log:
            return f"{self.args[0]}\n\n--- [BSPHP] 调试详情 ---\n{self.debug_log}"
        return str(self.args[0])


def _bsphp_truncate(s: str, max_len: int = 300) -> str:
    s = s.replace("\r", " ").replace("\n", " ")
    if len(s) <= max_len:
        return s
    return s[:max_len] + "..."


def _coerce_code(v: Any) -> Optional[int]:
    if v is None or isinstance(v, bool):
        return None
    if isinstance(v, int):
        return v
    if isinstance(v, float):
        return int(v)
    try:
        return int(v)
    except (TypeError, ValueError):
        return None


class BSPHPClient:
    def __init__(self, cfg: ClientConfig, machine_code_fn):
        self._cfg = cfg
        self._machine_code_fn = machine_code_fn
        self.bs_php_sessl = ""
        self.last_send_debug = ""
        self._session = requests.Session()
        self._session.headers.update(
            {"Content-Type": "application/x-www-form-urlencoded"}
        )

    def _bsphp_debug_enabled(self) -> bool:
        return os.environ.get("BSPHP_DEBUG", "1") != "0"

    def _bsphp_emit(self, lines: list[str], msg: str) -> None:
        lines.append(msg)
        if self._bsphp_debug_enabled():
            print(f"[BSPHP] {msg}", file=sys.stderr)

    @property
    def machine_code(self) -> str:
        return self._machine_code_fn()

    def send(self, api: str, params: Optional[dict[str, str]] = None) -> Optional[dict]:
        params = params or {}
        lines: list[str] = []
        now = datetime.now()
        appsafecode = md5_hex(now.strftime("%Y-%m-%d %H:%M:%S"))
        date_hash = now.strftime("%Y-%m-%d#%H:%M:%S")
        param: dict[str, str] = {
            "api": api,
            "BSphpSeSsL": self.bs_php_sessl,
            "date": date_hash,
            "md5": "",
            "mutualkey": self._cfg.mutual_key,
            "appsafecode": appsafecode,
        }
        param.update(params)
        pairs = [f"{k}={encode_parameter(v)}" for k, v in sorted(param.items())]
        data_str = "&".join(pairs)
        self._bsphp_emit(lines, "========== 加密前 ==========")
        self._bsphp_emit(lines, f"api: {api}")
        self._bsphp_emit(lines, f"request_data: {_bsphp_truncate(data_str, 2000)}")
        aes_key_full = md5_hex(self._cfg.server_private_key + appsafecode)
        aes_key = aes_key_full[:16]
        try:
            enc_b64 = aes128_cbc_encrypt_base64(data_str, aes_key)
            sig_md5 = md5_hex(enc_b64)
            signature_content = f"0|AES-128-CBC|{aes_key}|{sig_md5}|json"
            rsa_b64 = rsa_encrypt_pkcs1_base64(
                signature_content, self._cfg.client_public_key
            )
        except Exception as e:
            self._bsphp_emit(lines, "========== 请求加密失败（故无「加密后」「解密前」）==========")
            self._bsphp_emit(lines, f"api: {api} 错误: {e!r}")
            self._bsphp_emit(
                lines,
                "常见原因：clientPublicKey 须为后台「客户端公钥」；勿与 serverPrivateKey 对调；须与 mutualkey 对应应用一致",
            )
            self.last_send_debug = "\n".join(lines)
            return None
        payload = f"{enc_b64}|{rsa_b64}"
        encoded = quote(payload, safe="-_.~")
        self._bsphp_emit(lines, "========== 加密后 ==========")
        self._bsphp_emit(lines, f"encrypted_b64: {_bsphp_truncate(enc_b64, 120)}")
        self._bsphp_emit(lines, f"rsa_b64: {_bsphp_truncate(rsa_b64, 120)}")
        self._bsphp_emit(lines, f"payload 总长度: {len(payload)}, encoded 总长度: {len(encoded)}")
        try:
            r = self._session.post(
                self._cfg.url,
                data=f"parameter={encoded}",
                timeout=60,
            )
        except OSError as e:
            self._bsphp_emit(lines, f"HTTP 请求异常: {e!r}")
            self.last_send_debug = "\n".join(lines)
            return None
        self._bsphp_emit(lines, "========== 解密前(服务器原始响应) ==========")
        self._bsphp_emit(lines, f"http_status: {r.status_code}")
        if r.status_code != 200 or not r.content:
            self._bsphp_emit(lines, "响应非 200 或 body 为空，无法解密")
            self.last_send_debug = "\n".join(lines)
            return None
        text = r.text.strip()
        if not text:
            self._bsphp_emit(lines, "响应文本为空")
            self.last_send_debug = "\n".join(lines)
            return None
        raw = unquote(text) if "%" in text else text
        self._bsphp_emit(lines, f"raw 长度: {len(raw)}")
        self._bsphp_emit(lines, f"raw: {_bsphp_truncate(raw, 300)}")
        resp = decrypt_response_body(
            raw, self._cfg.server_private_key, appsafecode
        )
        if resp is None:
            self._bsphp_emit(lines, "========== 解密失败 ==========")
            self._bsphp_emit(
                lines,
                "无法解析响应（段数不足、RSA/AES 失败、JSON 非法或 appsafecode 不匹配）。请核对 serverPrivateKey / clientPublicKey / mutualkey / AppEn URL。",
            )
            self.last_send_debug = "\n".join(lines)
            return None
        self._bsphp_emit(lines, "========== 解密后 ==========")
        self._bsphp_emit(lines, f"response 摘要: code={resp.get('code')!r} data={_bsphp_truncate(str(resp.get('data')), 500)}")
        self.last_send_debug = "\n".join(lines)
        return resp

    def connect(self) -> bool:
        r = self.send("internet.in")
        return bool(r and str(r.get("data")) == "1")

    def get_sessl(self) -> bool:
        r = self.send("BSphpSeSsL.in")
        if not r:
            return False
        data = r.get("data")
        if isinstance(data, str) and data:
            self.bs_php_sessl = data
            return True
        return False

    def bootstrap(self) -> None:
        r = self.send("internet.in")
        log1 = self.last_send_debug
        if not r:
            raise InitFailedError("连接失败", debug_log=log1)
        if str(r.get("data")) != "1":
            raise InitFailedError(
                f"连接失败（internet.in 返回 data={r.get('data')!r}，期望 '1'）",
                debug_log=log1,
            )
        r = self.send("BSphpSeSsL.in")
        log2 = self.last_send_debug
        if not r:
            raise InitFailedError(
                "获取 BSphpSeSsL 失败",
                debug_log=f"--- internet.in ---\n{log1}\n\n--- BSphpSeSsL.in ---\n{log2}",
            )
        data = r.get("data")
        if isinstance(data, str) and data:
            self.bs_php_sessl = data
            return
        raise InitFailedError(
            f"获取 BSphpSeSsL 失败（data={data!r}，期望非空字符串）",
            debug_log=f"--- internet.in ---\n{log1}\n\n--- BSphpSeSsL.in ---\n{log2}",
        )

    def _result(self, api: str, params: Optional[dict[str, str]] = None) -> APIResult:
        r = self.send(api, params)
        if not r:
            return APIResult(None, None)
        return APIResult(r.get("data"), _coerce_code(r.get("code")))

    def logout(self) -> APIResult:
        r = self._result("cancellation.ic")
        self.bs_php_sessl = ""
        self.get_sessl()
        return r

    def add_card_features(
        self, carid: str, key: str, maxoror: str
    ) -> APIResult:
        return self._result(
            "AddCardFeatures.key.ic",
            {"carid": carid, "key": key, "maxoror": maxoror},
        )

    def recharge_card(self, icid: str, ka: str, pwd: str) -> APIResult:
        return self._result(
            "chong.ic", {"icid": icid, "ka": ka, "pwd": pwd}
        )

    def get_date_ic(self) -> APIResult:
        return self._result("getdate.ic")

    def get_login_info(self) -> APIResult:
        return self._result("getlkinfo.ic")

    def heartbeat(self) -> APIResult:
        return self._result("timeout.ic")

    def get_notice(self) -> APIResult:
        return self._result("gg.in")

    def get_version(self) -> APIResult:
        return self._result("v.in")

    def get_soft_info(self) -> APIResult:
        return self._result("miao.in")

    def get_server_date(self) -> APIResult:
        return self._result("date.in")

    def get_preset_url(self) -> APIResult:
        return self._result("url.in")

    def get_web_url(self) -> APIResult:
        return self._result("weburl.in")

    def get_global_info(self, info: str = "") -> APIResult:
        p = {"info": info} if info else {}
        return self._result("globalinfo.in", p)

    def get_app_custom(self, info: str) -> APIResult:
        return self._result("appcustom.in", {"info": info})

    def get_logic_a(self) -> APIResult:
        return self._result("logica.in")

    def get_logic_b(self) -> APIResult:
        return self._result("logicb.in")

    def query_card(self, cardid: str) -> APIResult:
        return self._result("socard.in", {"cardid": cardid})

    def get_card_info(
        self,
        ic_carid: str,
        ic_pwd: str,
        info: str,
        typ: Optional[str] = None,
    ) -> APIResult:
        p: dict[str, str] = {
            "ic_carid": ic_carid,
            "ic_pwd": ic_pwd,
            "info": info,
        }
        if typ:
            p["type"] = typ
        return self._result("getinfo.ic", p)

    def bind_card(self, key: str, icid: str, icpwd: str) -> APIResult:
        return self._result(
            "setcaron.ic", {"key": key, "icid": icid, "icpwd": icpwd}
        )

    def unbind_card(self, icid: str, icpwd: str) -> APIResult:
        return self._result(
            "setcarnot.ic", {"icid": icid, "icpwd": icpwd}
        )

    def login_ic(
        self,
        icid: str,
        icpwd: str = "",
        key: Optional[str] = None,
        maxoror: Optional[str] = None,
    ) -> APIResult:
        k = key if key is not None else self.machine_code
        m = maxoror if maxoror is not None else k
        r = self.send(
            "login.ic",
            {"icid": icid, "icpwd": icpwd, "key": k, "maxoror": m},
        )
        if not r:
            return APIResult("系统错误，登录失败！", None)
        ci = _coerce_code(r.get("code"))
        if ci in (1011, 9908, 1081) and isinstance(r.get("SeSsL"), str):
            self.bs_php_sessl = r["SeSsL"]
        return APIResult(r.get("data"), ci)
