#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations

import re
import secrets
from dataclasses import dataclass
from typing import Any
from urllib.parse import parse_qs, urlencode, urljoin, urlparse

import requests

from crypto_utils import decrypt_password, encrypt_password

DEFAULT_SPACE_ENDPOINT = "https://www.spacezs.cn/connect.php"
PROVIDER_LABELS = {"qq": "QQ", "wx": "微信", "alipay": "支付宝"}
SUPPORTED_PROVIDERS = set(PROVIDER_LABELS)
BIND_TOKEN_TTL_SECONDS = 600
SPACE_BROWSER_HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
    ),
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}


class SocialLoginError(Exception):
    def __init__(self, message: str, status_code: int = 400):
        super().__init__(message)
        self.message = message
        self.status_code = int(status_code or 400)


@dataclass
class SpaceProfile:
    provider: str
    social_uid: str
    nickname: str = ""
    avatar_url: str = ""


def provider_label(provider: str) -> str:
    return PROVIDER_LABELS.get(provider, provider)


def normalize_social_endpoint(value: str) -> str:
    raw = (value or DEFAULT_SPACE_ENDPOINT).strip() or DEFAULT_SPACE_ENDPOINT
    parsed = urlparse(raw)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        raise ValueError("聚合接口地址必须是 http 或 https 地址")
    path = parsed.path.rstrip("/")
    if path in {"", "/"}:
        return parsed._replace(path="/connect.php", params="", query="", fragment="").geturl()
    if path.endswith("/connect.php"):
        return parsed._replace(path=path, params="", query="", fragment="").geturl()
    raise ValueError("聚合接口地址必须指向 connect.php")


def parse_providers(value: str | list[str] | tuple[str, ...] | None) -> list[str]:
    items = value.split(",") if isinstance(value, str) else list(value or [])
    result: list[str] = []
    for item in items:
        provider = str(item or "").strip().lower()
        if provider in SUPPORTED_PROVIDERS and provider not in result:
            result.append(provider)
    return result


def social_appkey(config: dict[str, Any]) -> str:
    encrypted = str((config or {}).get("social_login_appkey") or "").strip()
    if not encrypted:
        return ""
    return decrypt_password(encrypted)


def encrypt_social_appkey(value: str) -> str:
    raw = str(value or "").strip()
    return encrypt_password(raw) if raw else ""


def mask_secret(value: str) -> str:
    if not value:
        return ""
    if len(value) <= 8:
        return f"{value[:2]}***"
    return f"{value[:4]}***{value[-4:]}"


def admin_social_config_out(config: dict[str, Any]) -> dict[str, Any]:
    key = social_appkey(config)
    return {
        "social_login_enabled": int(config.get("social_login_enabled") or 0),
        "social_login_endpoint": config.get("social_login_endpoint") or DEFAULT_SPACE_ENDPOINT,
        "social_login_appid": config.get("social_login_appid") or "",
        "social_login_appkey": "",
        "social_login_appkey_configured": bool(key),
        "social_login_appkey_masked": mask_secret(key),
        "social_login_providers": parse_providers(config.get("social_login_providers")) or ["qq", "wx", "alipay"],
    }


def public_social_config(config: dict[str, Any]) -> dict[str, Any]:
    providers = parse_providers(config.get("social_login_providers"))
    configured = bool(config.get("social_login_appid") and social_appkey(config))
    enabled = bool(int(config.get("social_login_enabled") or 0) == 1 and configured and providers)
    return {"enabled": enabled, "providers": providers if enabled else []}


def validate_social_ready(config: dict[str, Any], provider: str) -> None:
    provider = str(provider or "").strip().lower()
    if int(config.get("social_login_enabled") or 0) != 1:
        raise SocialLoginError("聚合登录未启用", 409)
    if provider not in parse_providers(config.get("social_login_providers")):
        raise SocialLoginError("该登录方式未启用", 400)
    if not config.get("social_login_appid") or not social_appkey(config):
        raise SocialLoginError("聚合登录配置不完整", 409)


def validate_redirect_uri(redirect_uri: str, *, allowed_hosts: set[str] | None = None) -> str:
    uri = str(redirect_uri or "").strip()
    parsed = urlparse(uri)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        raise SocialLoginError("redirect_uri 必须是完整的 http/https 地址", 400)
    if allowed_hosts:
        host = parsed.netloc.lower()
        hostname = (parsed.hostname or "").lower()
        if host not in allowed_hosts and hostname not in allowed_hosts:
            raise SocialLoginError("redirect_uri 域名不在允许范围内", 400)
    return uri


def _space_base_url(endpoint: str) -> str:
    parsed = urlparse(endpoint)
    return f"{parsed.scheme}://{parsed.netloc}"


def _parse_space_json(payload: dict[str, Any], error_message: str) -> dict[str, Any]:
    try:
        code_value = int(payload.get("code"))
    except (TypeError, ValueError):
        code_value = -1
    if code_value != 0:
        raise SocialLoginError(str(payload.get("msg") or error_message), 409)
    return payload


def parse_space_scan_page(html: str) -> tuple[str, str]:
    qrcode_match = re.search(r'var\s+qrcode_url\s*=\s*["\']([^"\']+)["\']', html or "")
    state_match = re.search(r'var\s+state\s*=\s*["\']([^"\']+)["\']', html or "")
    qrcode_url = qrcode_match.group(1).strip() if qrcode_match else ""
    state = state_match.group(1).strip() if state_match else ""
    if not qrcode_url or not state:
        raise SocialLoginError("聚合登录扫码页面解析失败", 409)
    return qrcode_url, state


def fetch_social_login_url(
    config: dict[str, Any],
    *,
    provider: str,
    mode: str,
    redirect_uri: str,
    allowed_hosts: set[str] | None = None,
) -> dict[str, Any]:
    provider = str(provider or "").strip().lower()
    mode = "bind" if str(mode or "").strip().lower() == "bind" else "login"
    validate_social_ready(config, provider)
    redirect_uri = validate_redirect_uri(redirect_uri, allowed_hosts=allowed_hosts)
    endpoint = normalize_social_endpoint(str(config.get("social_login_endpoint") or ""))
    params = {
        "act": "login",
        "appid": str(config.get("social_login_appid") or "").strip(),
        "appkey": social_appkey(config),
        "type": provider,
        "redirect_uri": redirect_uri,
    }
    try:
        response = requests.get(endpoint, params=params, timeout=15)
        payload = response.json()
    except ValueError as exc:
        raise SocialLoginError("聚合接口未返回 JSON，请检查 endpoint 是否为 connect.php", 409) from exc
    except requests.RequestException as exc:
        raise SocialLoginError("聚合接口连接失败", 409) from exc

    data = _parse_space_json(payload, "获取聚合登录地址失败")
    url = str(data.get("url") or "")
    if not url:
        raise SocialLoginError("聚合接口未返回授权地址", 409)

    result = {
        "provider": provider,
        "mode": mode,
        "url": url,
        "qrcode": str(data.get("qrcode") or ""),
        "scan_url": "",
        "scan_state": "",
        "scan_poll_interval": 2,
    }
    if provider == "wx":
        scan_page_url = result["qrcode"] or result["url"]
        try:
            scan_response = requests.get(scan_page_url, headers=SPACE_BROWSER_HEADERS, timeout=15)
        except requests.RequestException as exc:
            raise SocialLoginError("微信扫码页获取失败", 409) from exc
        scan_url, state = parse_space_scan_page(scan_response.text)
        result["scan_url"] = scan_url
        result["scan_state"] = state
    return result


def poll_social_scan(config: dict[str, Any], *, provider: str, state: str) -> dict[str, Any]:
    provider = str(provider or "").strip().lower()
    validate_social_ready(config, provider)
    if provider != "wx":
        raise SocialLoginError("该登录方式不需要轮询", 400)
    state_value = str(state or "").strip()
    if not state_value:
        raise SocialLoginError("缺少扫码状态", 400)

    endpoint = normalize_social_endpoint(str(config.get("social_login_endpoint") or ""))
    base_url = _space_base_url(endpoint)
    ajax_url = urljoin(base_url + "/", "ajax.php")
    headers = {
        **SPACE_BROWSER_HEADERS,
        "Referer": f"{base_url}/jump.php?state={state_value}&client=1",
        "X-Requested-With": "XMLHttpRequest",
        "Accept": "application/json, text/javascript, */*; q=0.01",
    }
    try:
        response = requests.get(ajax_url, params={"act": "login", "state": state_value}, headers=headers, timeout=10)
        payload = response.json()
    except ValueError as exc:
        raise SocialLoginError("扫码轮询未返回 JSON", 409) from exc
    except requests.RequestException as exc:
        raise SocialLoginError("扫码轮询连接失败", 409) from exc

    try:
        code_value = int(payload.get("code"))
    except (TypeError, ValueError):
        code_value = -1
    if code_value == 1:
        return {"status": "pending"}
    if code_value == 0:
        url = str(payload.get("url") or "")
        if not url:
            raise SocialLoginError("扫码成功但未返回回调地址", 409)
        return {"status": "authorized", "url": url}
    raise SocialLoginError(str(payload.get("msg") or "扫码登录失败"), 409)


def _profile_uid(payload: dict[str, Any]) -> str:
    for key in ("social_uid", "uid", "openid", "unionid", "id"):
        value = str(payload.get(key) or "").strip()
        if value:
            return value
    raise SocialLoginError("聚合回调未返回用户唯一标识", 409)


def fetch_space_profile(config: dict[str, Any], *, provider: str, code: str) -> SpaceProfile:
    provider = str(provider or "").strip().lower()
    code_value = str(code or "").strip()
    validate_social_ready(config, provider)
    if not code_value:
        raise SocialLoginError("缺少授权 code", 400)
    endpoint = normalize_social_endpoint(str(config.get("social_login_endpoint") or ""))
    params = {
        "act": "callback",
        "appid": str(config.get("social_login_appid") or "").strip(),
        "appkey": social_appkey(config),
        "type": provider,
        "code": code_value,
    }
    try:
        response = requests.get(endpoint, params=params, timeout=15)
        payload = response.json()
    except ValueError as exc:
        raise SocialLoginError("聚合回调接口未返回 JSON", 409) from exc
    except requests.RequestException as exc:
        raise SocialLoginError("聚合回调接口连接失败", 409) from exc

    data = _parse_space_json(payload, "聚合登录回调失败")
    return SpaceProfile(
        provider=provider,
        social_uid=_profile_uid(data),
        nickname=str(data.get("nickname") or data.get("nick") or data.get("name") or ""),
        avatar_url=str(data.get("faceimg") or data.get("avatar") or data.get("avatar_url") or ""),
    )


def callback_mode_from_redirect_query(redirect_url: str, fallback: str = "login") -> str:
    parsed = urlparse(redirect_url)
    mode = parse_qs(parsed.query).get("mode", [fallback])[0]
    return "bind" if mode == "bind" else "login"


def append_query(url: str, values: dict[str, str]) -> str:
    parsed = urlparse(url)
    query = parse_qs(parsed.query)
    for key, value in values.items():
        query[key] = [value]
    return parsed._replace(query=urlencode(query, doseq=True)).geturl()


def new_bind_token() -> str:
    return secrets.token_urlsafe(32)
