from __future__ import annotations

import json
import re
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from email.utils import parsedate_to_datetime
from html.parser import HTMLParser
from http.cookies import SimpleCookie
from typing import Any, Dict, Optional

import yaml
from prisma import Prisma, Json
from jsonschema import validate as jsonschema_validate, ValidationError
from prisma.models import Platform, AuthMethod, Secret
import httpx
from loguru import logger
from turbo_agent_auth.logging import (
    log_login_step,
    log_login_final,
    log_login_failure,
    log_sensitive_payload,
    log_step,
)

from turbo_agent_auth.config.runtime import get_callback_url, get_ip_allow_list
from turbo_agent_auth.schemas import (
    AuthMethodCreate,
    AuthMethodUpdate,
    PlatformCreate,
    PlatformUpdate,
    SecretUpsert,
    SecretUpdate,
    SecretRemindRequest,
    ParseResponseRequest,
    ParseResponseResult,
    LoginStepRequestSnapshot,
    LoginStepResult,
    LoginExecutionResult,
    DefaultSchemaOut,
    AuthType,
)
from turbo_agent_auth.auth_core import (
    AuthFlowExecutor,
    ResponseData,
    parse_response_data,
    prepare_login_request_config,
    _apply_runtime_defaults,
)

# 加载默认schema配置
default_auth_schemas: Dict[str, Dict[str, Any]] = {}




def _collect_response_headers(response: httpx.Response) -> Dict[str, Any]:
    headers: Dict[str, Any] = {}
    for key, value in response.headers.multi_items():
        if key in headers:
            existing = headers[key]
            if isinstance(existing, list):
                existing.append(value)
            else:
                headers[key] = [existing, value]
        else:
            headers[key] = value
    return headers


def _to_prisma_json(value: Any) -> Any:
    """Wrap dict/list payloads in Prisma Json to keep header names etc. intact."""
    if value is None or isinstance(value, Json):
        return value
    if isinstance(value, (dict, list)):
        return Json(value)
    return value




def _format_validation_error(error: ValidationError) -> str:
    path = ".".join(str(p) for p in error.absolute_path)
    if error.validator == "required":
        missing = None
        if error.message.count("'") >= 2:
            missing = error.message.split("'")[1]
        elif isinstance(error.validator_value, list) and error.validator_value:
            missing = ", ".join(error.validator_value)
        field_desc = missing or path or "字段"
        return f"缺少必填字段: {field_desc}"
    if error.validator == "type":
        expected = error.validator_value
        field_desc = path or "字段"
        return f"字段 {field_desc} 类型应为 {expected}"
    if error.validator == "format":
        field_desc = path or "字段"
        return f"字段 {field_desc} 不符合格式要求 ({error.validator_value})"
    return error.message


def load_default_schemas() -> Dict[str, Dict[str, Any]]:
    global default_auth_schemas
    if default_auth_schemas:
        return default_auth_schemas
    from importlib import resources

    with resources.files("turbo_agent_auth.config").joinpath("auth_schemas.yaml").open("r", encoding="utf-8") as f:
        data = yaml.safe_load(f) or {}
    default_auth_schemas.update(data)
    return default_auth_schemas


def get_default_schema(auth_type: AuthType) -> Optional[DefaultSchemaOut]:
    data = load_default_schemas().get(auth_type.value)
    if not data:
        return None
    return DefaultSchemaOut(
        type=auth_type,
        description=data.get("description"),
        loginFlow=data.get("loginFlow"),
        loginFieldsSchema=data.get("loginFieldsSchema"),
        refreshFlow=data.get("refreshFlow"),
        refreshFieldsSchema=data.get("refreshFieldsSchema"),
        authFieldsSchema=data.get("authFieldsSchema"),
        authFieldPlacements=data.get("authFieldPlacements"),
        responseMapping=data.get("responseMapping"),
    )


class AuthService:
    def __init__(self, client: Prisma):
        self.client = client

    # 平台相关
    async def create_platform(self, payload: PlatformCreate) -> Platform:
        return await self.client.platform.create(
            data={
                "id": payload.id,
                "orgId": payload.orgId,
                "nameId": payload.nameId,
                "name": payload.name,
                "code": payload.code,
                "baseUrl": str(payload.baseUrl) if payload.baseUrl else None,
                "description": payload.description,
                "isActive": payload.isActive if payload.isActive is not None else True,
            }
        )

    async def list_platforms(self) -> list[Platform]:
        return await self.client.platform.find_many(order={"createdAt": "desc"})

    async def get_platform(self, platform_id: str) -> Optional[Platform]:
        return await self.client.platform.find_unique(where={"id": platform_id})

    async def update_platform(self, platform_id: str, payload: PlatformUpdate) -> Optional[Platform]:
        data: Dict[str, Any] = {}
        for k, v in payload.model_dump(exclude_none=True).items():
            if k == "baseUrl" and v is not None:
                data[k] = str(v)
            else:
                data[k] = v
        if not data:
            return await self.get_platform(platform_id)
        return await self.client.platform.update(where={"id": platform_id}, data=data)

    # 授权方式
    async def create_auth_method(self, payload: AuthMethodCreate) -> AuthMethod:
        # 如果未提供 flow 相关配置，加载默认
        login_flow = payload.loginFlow
        login_fields_schema = payload.loginFieldsSchema
        refresh_flow = payload.refreshFlow
        refresh_fields_schema = payload.refreshFieldsSchema
        auth_fields_schema = payload.authFieldsSchema
        auth_field_placements = payload.authFieldPlacements
        response_mapping = payload.responseMapping

        default_schema = default_auth_schemas.get(payload.authType.value)
        if default_schema:
            if not login_flow and "loginFlow" in default_schema:
                login_flow = default_schema["loginFlow"]
            if not login_fields_schema and "loginFieldsSchema" in default_schema:
                login_fields_schema = default_schema["loginFieldsSchema"]
            if not refresh_flow and "refreshFlow" in default_schema:
                refresh_flow = default_schema["refreshFlow"]
            if not refresh_fields_schema and "refreshFieldsSchema" in default_schema:
                refresh_fields_schema = default_schema["refreshFieldsSchema"]
            if not auth_fields_schema and "authFieldsSchema" in default_schema:
                auth_fields_schema = default_schema["authFieldsSchema"]
            if not auth_field_placements and "authFieldPlacements" in default_schema:
                auth_field_placements = default_schema["authFieldPlacements"]
            if not response_mapping and "responseMapping" in default_schema:
                response_mapping = default_schema["responseMapping"]

        data: Dict[str, Any] = {
            "id": payload.id,
            "platform": {"connect": {"id": payload.platformId}},
            "name": payload.name,
            "type": payload.authType.value,
            "description": payload.description,
            "defaultValiditySeconds": payload.defaultValiditySeconds,
            "refreshBeforeSeconds": payload.refreshBeforeSeconds,
        }

        json_fields = {
            "loginFlow": login_flow,
            "loginFieldsSchema": login_fields_schema,
            "refreshFlow": refresh_flow,
            "refreshFieldsSchema": refresh_fields_schema,
            "authFieldsSchema": auth_fields_schema,
            "authFieldPlacements": auth_field_placements,
            "responseMapping": response_mapping,
        }
        for key, value in json_fields.items():
            if value is not None:
                data[key] = _to_prisma_json(value)

        return await self.client.authmethod.create(data=data)

    async def update_auth_method(self, auth_method_id: str, payload: AuthMethodUpdate) -> Optional[AuthMethod]:
        data: Dict[str, Any] = payload.model_dump(exclude_none=True)
        for key in (
            "loginFlow",
            "loginFieldsSchema",
            "refreshFlow",
            "refreshFieldsSchema",
            "authFieldsSchema",
            "authFieldPlacements",
            "responseMapping",
        ):
            if key in data:
                data[key] = _to_prisma_json(data[key])
        if not data:
            return await self.client.authmethod.find_unique(where={"id": auth_method_id})
        return await self.client.authmethod.update(where={"id": auth_method_id}, data=data)

    async def list_auth_methods(self, platform_id: Optional[str] = None) -> list[AuthMethod]:
        where = {"platformId": platform_id} if platform_id else {}
        return await self.client.authmethod.find_many(where=where, order={"createdAt": "desc"})

    async def get_auth_method(self, auth_method_id: str) -> Optional[AuthMethod]:
        return await self.client.authmethod.find_unique(where={"id": auth_method_id})

    # 秘钥相关（1:N by (authMethodId, name)）
    async def upsert_secret(self, auth_method: AuthMethod, payload: SecretUpsert) -> Secret:
        platform_id = auth_method.platformId
        auth_method_id = auth_method.id
        login_schema = auth_method.loginFieldsSchema if isinstance(auth_method.loginFieldsSchema, dict) else None
        auth_fields_schema = auth_method.authFieldsSchema if isinstance(auth_method.authFieldsSchema, dict) else None

        # autoLoginEnabled=False 时，不校验也不保存 loginPayload
        should_validate_login = (payload.autoLoginEnabled is not False)

        if login_schema and should_validate_login:
            if payload.loginPayload is None:
                # 创建时强制要求提供 loginPayload 用于首次授权（除非明确禁用自动登录）
                raise ValueError("创建秘钥必须提供 loginPayload 以完成首次授权")
            payload_to_validate = _apply_runtime_defaults(payload.loginPayload) or {}
            try:
                jsonschema_validate(payload_to_validate, login_schema)
            except ValidationError as exc:
                raise ValueError(_format_validation_error(exc))

        if auth_fields_schema:
            try:
                jsonschema_validate(payload.credential, auth_fields_schema)
            except ValidationError as exc:
                raise ValueError(_format_validation_error(exc))

        if not payload.name:
            raise ValueError("秘钥名称 name 必填")
        # 取消唯一约束后，这里取同名最新一条用于 upsert 语义
        existing_list = await self.client.secret.find_many(
            where={"authMethodId": auth_method_id, "name": payload.name},
            order={"createdAt": "desc"},
            take=1,
        )
        existing = existing_list[0] if existing_list else None
        # 不存储 loginPayload 当 autoLoginEnabled 为 False
        store_login_payload = bool(payload.autoLoginEnabled)

        data = {
            # 关系需通过 connect 赋值
            "platform": {"connect": {"id": platform_id}},
            "authMethod": {"connect": {"id": auth_method_id}},
            "name": payload.name,
            "tags": payload.tags or [],
            "data": _to_prisma_json(payload.credential),
            **(
                {"loginPayload": _to_prisma_json(payload.loginPayload)}
                if (payload.loginPayload is not None and store_login_payload)
                else {}
            ),
            "expiresAt": payload.expiresAt,
            "validFrom": payload.validFrom,
            "status": payload.status or "active",
            "lastRefreshed": datetime.now(timezone.utc),
            # 用户绑定（不保存用户名/密码）
            "userEmail": payload.userEmail,
        }
        manual_sources = {str(k): "manual-input" for k in (payload.credential or {}).keys()}
        if manual_sources:
            data["authDataSources"] = _to_prisma_json(manual_sources)
        if payload.autoLoginEnabled is not None:
            data["autoLoginEnabled"] = payload.autoLoginEnabled
        elif not existing:
            data["autoLoginEnabled"] = False
        if existing:
            return await self.client.secret.update(where={"id": existing.id}, data=data)
        if not payload.id:
            raise ValueError("创建秘钥时必须提供 id")
        data["id"] = payload.id
        return await self.client.secret.create(data=data)

    async def list_secrets(self, auth_method_id: str) -> list[Secret]:
        return await self.client.secret.find_many(
            where={"authMethodId": auth_method_id}, order={"createdAt": "desc"}
        )

    async def get_secret(self, auth_method_id: str, name: Optional[str] = None) -> Optional[Secret]:
        if name:
            items = await self.client.secret.find_many(
                where={"authMethodId": auth_method_id, "name": name},
                order={"createdAt": "desc"},
                take=1,
            )
            return items[0] if items else None
        # 未指定名称则返回最新一个（兼容旧接口行为）
        items = await self.list_secrets(auth_method_id)
        return items[0] if items else None

    async def get_secret_by_id(self, secret_id: str) -> Optional[Secret]:
        return await self.client.secret.find_unique(where={"id": secret_id})

    async def update_secret(self, secret_id: str, data: Dict[str, Any]) -> Optional[Secret]:
        if not data:
            return await self.get_secret_by_id(secret_id)

        # Map 'credential' to 'data' for Prisma compatibility
        if "credential" in data:
            data["data"] = data.pop("credential")

        secret = await self.get_secret_by_id(secret_id)
        if not secret:
            return None

        auth_method = await self.get_auth_method(secret.authMethodId)
        login_schema = (
            auth_method.loginFieldsSchema
            if auth_method and isinstance(auth_method.loginFieldsSchema, dict)
            else None
        )
        auth_fields_schema = (
            auth_method.authFieldsSchema
            if auth_method and isinstance(auth_method.authFieldsSchema, dict)
            else None
        )

        # 检查是否需要校验登录字段：仅当 autoLoginEnabled 不为 False 时校验
        # 注意：SecretUpdate 中 autoLoginEnabled 是 Optional，若未提供则沿用 secret.autoLoginEnabled
        new_auto_login = data.get("autoLoginEnabled")
        effective_auto_login = new_auto_login if new_auto_login is not None else secret.autoLoginEnabled
        should_validate_login = (effective_auto_login is not False)

        if login_schema and ("loginPayload" in data or "autoLoginEnabled" in data) and should_validate_login:
            candidate_payload = data.get("loginPayload", secret.loginPayload)
            if effective_auto_login is False:
                # 禁用自动登录时不允许持久化 loginPayload
                if "loginPayload" in data:
                    data.pop("loginPayload", None)
            else:
                candidate_payload = _apply_runtime_defaults(candidate_payload) or {}
                try:
                    jsonschema_validate(candidate_payload, login_schema)
                except ValidationError as exc:
                    raise ValueError(_format_validation_error(exc))

        if auth_fields_schema and "data" in data:
            try:
                jsonschema_validate(data["data"], auth_fields_schema)
            except ValidationError as exc:
                raise ValueError(_format_validation_error(exc))

        if "data" in data:
            manual_sources = {str(k): "manual-input" for k in data["data"].keys()}
            if manual_sources:
                combined_sources: Dict[str, Any] = {}
                if isinstance(secret.authDataSources, dict):
                    combined_sources.update(secret.authDataSources)
                existing_update_sources = data.get("authDataSources")
                if isinstance(existing_update_sources, dict):
                    combined_sources.update(existing_update_sources)
                combined_sources.update(manual_sources)
                data["authDataSources"] = _to_prisma_json(combined_sources)

        # 移除不再支持的凭证字段
        for deprecated in ("credentialUsername", "credentialPassword", "credentialConsent"):
            if deprecated in data:
                data.pop(deprecated, None)
        # 如果 autoLoginEnabled 显式为 False，清除已存储 loginPayload
        if data.get("autoLoginEnabled") is False:
            data["loginPayload"] = None

        json_keys = {"data", "loginPayload", "authDataSources"}
        for key in list(data.keys()):
            if key in json_keys:
                data[key] = _to_prisma_json(data[key])

        return await self.client.secret.update(where={"id": secret_id}, data=data)

    async def remind_secret(self, secret_id: str, req: SecretRemindRequest) -> Optional[Secret]:
        now = datetime.now(timezone.utc)
        data: Dict[str, Any] = {"lastReminderAt": now}
        if req.email:
            data["userEmail"] = req.email
        return await self.client.secret.update(where={"id": secret_id}, data=data)

    async def delete_secret(self, secret_id: str) -> bool:
        s = await self.get_secret_by_id(secret_id)
        if not s:
            return False
        await self.client.secret.delete(where={"id": secret_id})
        return True

    async def invalidate_secret(self, secret_id: str, reason: Optional[str] = None) -> Optional[Secret]:
        data: Dict[str, Any] = {"isValid": False, "status": "invalid", "invalidNotified": False}
        if reason:
            data["lastLoginError"] = reason
            data["lastLoginStatus"] = "failed"
        return await self.client.secret.update(where={"id": secret_id}, data=data)

    async def mark_invalid_notified(self, secret_id: str, notified: bool) -> Optional[Secret]:
        return await self.client.secret.update(where={"id": secret_id}, data={"invalidNotified": bool(notified)})

    async def _parse_response_internal(
        self,
        mapping: Optional[Dict[str, Any]],
        body: Optional[Dict[str, Any]],
        headers: Optional[Dict[str, Any]],
        cookies: Optional[Dict[str, Any]],
        raw_body: str,
        auth_method: Optional[AuthMethod] = None
    ) -> ParseResponseResult:
        default_validity = auth_method.defaultValiditySeconds if auth_method else None
        result = parse_response_data(
            mapping, body, headers, cookies, raw_body, default_validity_seconds=default_validity
        )
        return ParseResponseResult(
            extracted=result.extracted,
            fieldSources=result.fieldSources,
            expiresAt=result.expiresAt
        )

    # 解析返回结果
    async def parse_response(self, req: ParseResponseRequest) -> ParseResponseResult:
        am = await self.get_auth_method(req.authMethodId)
        if not am:
            raise ValueError("授权方式不存在")
        
        body = req.body or {}
        headers = (req.headers or {}) if isinstance(req.headers, dict) else {}
        cookies_in = (req.cookies or {}) if isinstance(req.cookies, dict) else {}
        raw_body = req.rawBody or ""
        if not raw_body and body:
            try:
                raw_body = json.dumps(body)
            except Exception:
                raw_body = ""
                
        return await self._parse_response_internal(
            am.responseMapping, body, headers, cookies_in, raw_body, am
        )

    async def pre_auth_login_flow(
        self,
        auth_method: AuthMethod,
        login_payload: Dict[str, Any],
        proxy: Optional[str] = None,
    ) -> LoginExecutionResult:
        """
        执行预授权登录流程，不涉及 Secret 数据库记录。
        """
        effective_payload = _apply_runtime_defaults(deepcopy(login_payload))
        flow_config = auth_method.loginFlow
        steps = flow_config.get("steps") if isinstance(flow_config, dict) else None

        execution_steps: list[LoginStepResult] = []
        final_extracted: Dict[str, Any] = {}
        final_field_sources: Dict[str, str] = {}
        final_expires_at: Optional[datetime] = None
        final_response = None
        
        # 调试/脱敏输出登录表单内容
        log_sensitive_payload(login_payload)

        try:
            if steps and isinstance(steps, list):
                for idx, step in enumerate(steps):
                    if not isinstance(step, dict):
                        continue
                    
                    req_cfg = prepare_login_request_config(step, effective_payload)
                    
                    # 记录请求快照
                    req_snapshot = LoginStepRequestSnapshot(
                        method=req_cfg["method"],
                        url=req_cfg["url"],
                        headers=req_cfg["headers"],
                        params=req_cfg["params"],
                        cookies=req_cfg.get("cookies"),
                        json_body=req_cfg["json"],
                        data=req_cfg["data"],
                    )

                    start_ts = datetime.now(timezone.utc)
                    step_error = None
                    response = None
                    try:
                        async with httpx.AsyncClient(timeout=req_cfg.get("timeout", 30.0), follow_redirects=bool(req_cfg.get("allow_redirects", True)), proxy=proxy) as http:
                            response = await http.request(
                                method=req_cfg["method"],
                                url=req_cfg["url"],
                                headers=req_cfg["headers"],
                                params=req_cfg["params"],
                                json=req_cfg["json"],
                                data=req_cfg["data"],
                                files=req_cfg.get("files"),
                                cookies=req_cfg.get("cookies"),
                            )
                    except Exception as e:
                        step_error = str(e)
                    finally:
                        end_ts = datetime.now(timezone.utc)
                    
                    duration_ms = int((end_ts - start_ts).total_seconds() * 1000)
                    final_response = response

                    step_result = LoginStepResult(
                        index=idx,
                        name=step.get("name") or f"step_{idx}",
                        request=req_snapshot,
                        statusCode=response.status_code if response else None,
                        ok=(response is not None and response.is_success),
                        durationMs=duration_ms,
                        error=step_error,
                    )

                    if step_error or not response:
                        execution_steps.append(step_result)
                        raise ValueError(f"Step {idx} failed: {step_error or 'No response'}")

                    if not response.is_success:
                        execution_steps.append(step_result)
                        raise ValueError(f"Step {idx} failed with status {response.status_code}")

                    # 解析步骤响应
                    step_mapping = step.get("responseMapping")
                    if step_mapping:
                        body_json = None
                        try:
                            body_json = response.json()
                        except Exception:
                            pass
                        headers_dict = _collect_response_headers(response)
                        cookies_dict = {k: v for k, v in response.cookies.items()}
                        raw_body = response.text
                        parsed = await self._parse_response_internal(step_mapping, body_json, headers_dict, cookies_dict, raw_body, auth_method)
                        
                        effective_payload.update(parsed.extracted)
                        final_extracted.update(parsed.extracted)
                        final_field_sources.update(parsed.fieldSources)
                        
                        step_result.extracted = parsed.extracted
                        step_result.fieldSources = parsed.fieldSources
                    
                    execution_steps.append(step_result)

            else:
                # 单步流程
                if not flow_config:
                    raise ValueError("未配置登录流程")
                
                req_cfg = prepare_login_request_config(flow_config, effective_payload)
                req_snapshot = LoginStepRequestSnapshot(
                    method=req_cfg["method"],
                    url=req_cfg["url"],
                    headers=req_cfg["headers"],
                    params=req_cfg["params"],
                    cookies=req_cfg.get("cookies"),
                    json_body=req_cfg["json"],
                    data=req_cfg["data"],
                )

                start_ts = datetime.now(timezone.utc)
                step_error = None
                response = None
                try:
                    async with httpx.AsyncClient(timeout=req_cfg.get("timeout", 30.0), follow_redirects=bool(req_cfg.get("allow_redirects", True)), proxy=proxy) as http:
                        response = await http.request(
                            method=req_cfg["method"],
                            url=req_cfg["url"],
                            headers=req_cfg["headers"],
                            params=req_cfg["params"],
                            json=req_cfg["json"],
                            data=req_cfg["data"],
                            files=req_cfg.get("files"),
                            cookies=req_cfg.get("cookies"),
                        )
                except Exception as e:
                    step_error = str(e)
                finally:
                    end_ts = datetime.now(timezone.utc)
                
                duration_ms = int((end_ts - start_ts).total_seconds() * 1000)
                final_response = response

                step_result = LoginStepResult(
                    index=0,
                    name=flow_config.get("name") or "single",
                    request=req_snapshot,
                    statusCode=response.status_code if response else None,
                    ok=(response is not None and response.is_success),
                    durationMs=duration_ms,
                    error=step_error,
                )
                execution_steps.append(step_result)

                if step_error or not response:
                    raise ValueError(f"Login request failed: {step_error or 'No response'}")
                
                if not response.is_success:
                    raise ValueError(f"Login request failed with status {response.status_code}")

            # 最终解析
            if auth_method.responseMapping and final_response:
                body_json = None
                try:
                    body_json = final_response.json()
                except Exception:
                    pass
                headers_dict = _collect_response_headers(final_response)
                cookies_dict = {k: v for k, v in final_response.cookies.items()}
                raw_body = final_response.text
                parsed = await self._parse_response_internal(auth_method.responseMapping, body_json, headers_dict, cookies_dict, raw_body, auth_method)
                
                final_extracted.update(parsed.extracted)
                final_field_sources.update(parsed.fieldSources)
                final_expires_at = parsed.expiresAt

            return LoginExecutionResult(
                steps=execution_steps,
                finalExtracted=final_extracted,
                finalFieldSources=final_field_sources,
                finalExpiresAt=final_expires_at,
                success=True,
            )

        except Exception as e:
            return LoginExecutionResult(
                steps=execution_steps,
                finalExtracted=final_extracted,
                finalFieldSources=final_field_sources,
                finalExpiresAt=final_expires_at,
                success=False,
                error=str(e),
            )

    async def _execute_login_request(self, flow: Dict[str, Any], payload: Dict[str, Any], proxy: Optional[str] = None) -> httpx.Response:
        request_cfg = prepare_login_request_config(flow, payload)
        timeout = request_cfg.get("timeout", 30.0)
        allow_redirects = bool(request_cfg.get("allow_redirects", True))
        async with httpx.AsyncClient(timeout=timeout, follow_redirects=allow_redirects, proxy=proxy) as client:
            return await client.request(
                method=request_cfg["method"],
                url=request_cfg["url"],
                headers=request_cfg["headers"],
                params=request_cfg["params"],
                json=request_cfg["json"],
                data=request_cfg["data"],
                files=request_cfg.get("files"),
                cookies=request_cfg.get("cookies"),
            )

    async def perform_login(
        self,
        secret_id: str,
        login_payload: Optional[Dict[str, Any]] = None,
        store_login_payload: bool = False,
        proxy: Optional[str] = None,
    ) -> Secret:
        secret = await self.get_secret_by_id(secret_id)
        if not secret:
            raise ValueError("秘钥不存在")

        auth_method = await self.get_auth_method(secret.authMethodId)
        if not auth_method:
            raise ValueError("授权方式不存在")

        # 若未开启自动登录且未存储 loginPayload，则必须用户本次提交
        if not secret.autoLoginEnabled and (secret.loginPayload is None) and not login_payload:
            raise ValueError("本次登录需要提供 loginPayload 字段")

        effective_payload_raw = deepcopy(secret.loginPayload) if isinstance(secret.loginPayload, dict) else {}
        if login_payload:
            effective_payload_raw.update(login_payload)

        effective_payload = _apply_runtime_defaults(effective_payload_raw)

        flow_config = auth_method.loginFlow
        steps = flow_config.get("steps") if isinstance(flow_config, dict) else None

        final_response = None
        extracted_data: Dict[str, Any] = {}
        field_sources: Dict[str, str] = {}
        expires_at: Optional[datetime] = None
        # 调试/脱敏输出登录表单内容
        log_sensitive_payload(effective_payload_raw)

        try:
            if steps and isinstance(steps, list):
                for idx, step in enumerate(steps):
                    if not isinstance(step, dict):
                        continue
                    req_cfg = prepare_login_request_config(step, effective_payload)
                    start_ts = datetime.now(timezone.utc)
                    try:
                        async with httpx.AsyncClient(timeout=req_cfg.get("timeout", 30.0), follow_redirects=bool(req_cfg.get("allow_redirects", True)), proxy=proxy) as http:
                            response = await http.request(
                                method=req_cfg["method"],
                                url=req_cfg["url"],
                                headers=req_cfg["headers"],
                                params=req_cfg["params"],
                                json=req_cfg["json"],
                                data=req_cfg["data"],
                                cookies=req_cfg.get("cookies"),
                            )
                    finally:
                        end_ts = datetime.now(timezone.utc)
                    duration_ms = int((end_ts - start_ts).total_seconds() * 1000)
                    final_response = response
                    log_login_step(
                        idx,
                        step.get("name") or f"step_{idx}",
                        req_cfg.get("method"),
                        req_cfg.get("url"),
                        response.status_code,
                        duration_ms,
                    )
                    step_mapping = step.get("responseMapping")
                    if step_mapping:
                        body_json = None
                        try:
                            body_json = response.json()
                        except Exception:
                            pass
                        headers_dict = _collect_response_headers(response)
                        cookies_dict = {k: v for k, v in response.cookies.items()}
                        raw_body = response.text
                        parsed = await self._parse_response_internal(step_mapping, body_json, headers_dict, cookies_dict, raw_body, auth_method)
                        effective_payload.update(parsed.extracted)
                        extracted_data.update(parsed.extracted)
                        field_sources.update(parsed.fieldSources)
                        log_step(
                            "debug",
                            "login.step.parsed",
                            step_index=idx,
                            keys=list(parsed.extracted.keys()),
                            expires_at=parsed.expiresAt.isoformat() if parsed.expiresAt else None,
                        )
            else:
                if not flow_config:
                    raise ValueError("未配置登录流程")
                req_cfg = prepare_login_request_config(flow_config, effective_payload)
                start_ts = datetime.now(timezone.utc)
                try:
                    async with httpx.AsyncClient(timeout=req_cfg.get("timeout", 30.0), follow_redirects=bool(req_cfg.get("allow_redirects", True)), proxy=proxy) as http:
                        response = await http.request(
                            method=req_cfg["method"],
                            url=req_cfg["url"],
                            headers=req_cfg["headers"],
                            params=req_cfg["params"],
                            json=req_cfg["json"],
                            data=req_cfg["data"],
                            cookies=req_cfg.get("cookies"),
                        )
                finally:
                    end_ts = datetime.now(timezone.utc)
                duration_ms = int((end_ts - start_ts).total_seconds() * 1000)
                log_login_step(
                    0,
                    flow_config.get("name") or "single",
                    req_cfg.get("method"),
                    req_cfg.get("url"),
                    response.status_code,
                    duration_ms,
                )
                final_response = response

            if auth_method.responseMapping and final_response:
                body_json = None
                try:
                    body_json = final_response.json()
                except Exception:
                    pass
                headers_dict = _collect_response_headers(final_response)
                cookies_dict = {k: v for k, v in final_response.cookies.items()}
                raw_body = final_response.text
                parsed = await self._parse_response_internal(auth_method.responseMapping, body_json, headers_dict, cookies_dict, raw_body, auth_method)
                extracted_data.update(parsed.extracted)
                field_sources.update(parsed.fieldSources)
                expires_at = parsed.expiresAt
                log_login_final(
                    list(parsed.extracted.keys()),
                    parsed.expiresAt.isoformat() if parsed.expiresAt else None,
                )
        except Exception as e:
            # 标记登录失败与账号无效
            log_login_failure(str(e))
            await self.client.secret.update(
                where={"id": secret_id},
                data={
                    "isValid": False,
                    "invalidNotified": False,
                    "lastLoginAt": datetime.now(timezone.utc),
                    "lastLoginStatus": "failed",
                    "lastLoginError": str(e),
                    "status": "invalid",
                },
            )
            raise

        update_data: Dict[str, Any] = {
            "data": _to_prisma_json(extracted_data),
            "authDataSources": _to_prisma_json(field_sources),
            "lastRefreshed": datetime.now(timezone.utc),
            "status": "active",
            "isValid": True,
            "invalidNotified": False,
            "lastLoginAt": datetime.now(timezone.utc),
            "lastLoginStatus": "success",
            "lastLoginError": None,
        }
        if expires_at:
            update_data["expiresAt"] = expires_at

        if store_login_payload:
            update_data["loginPayload"] = _to_prisma_json(effective_payload_raw)

        return await self.client.secret.update(where={"id": secret_id}, data=update_data)

    async def perform_login_with_trace(
        self,
        secret_id: str,
        login_payload: Optional[Dict[str, Any]] = None,
        store_login_payload: bool = False,
        proxy: Optional[str] = None,
    ) -> tuple[Secret, LoginExecutionResult]:
        secret = await self.get_secret_by_id(secret_id)
        if not secret:
            raise ValueError("秘钥不存在")

        auth_method = await self.get_auth_method(secret.authMethodId)
        if not auth_method:
            raise ValueError("授权方式不存在")

        if not secret.autoLoginEnabled and (secret.loginPayload is None) and not login_payload:
            raise ValueError("本次登录需要提供 loginPayload 字段")

        effective_payload_raw = deepcopy(secret.loginPayload) if isinstance(secret.loginPayload, dict) else {}
        if login_payload:
            effective_payload_raw.update(login_payload)
        effective_payload = _apply_runtime_defaults(effective_payload_raw)

        flow_config = auth_method.loginFlow
        steps_conf = flow_config.get("steps") if isinstance(flow_config, dict) else None

        exec_result = LoginExecutionResult()
        final_response = None

        # 调试/脱敏输出登录表单内容
        log_sensitive_payload(effective_payload_raw)

        async def _run_one(idx: int, step_conf: Dict[str, Any]):
            req_cfg = prepare_login_request_config(step_conf, effective_payload)
            snapshot = LoginStepRequestSnapshot(
                method=req_cfg["method"],
                url=req_cfg["url"],
                headers=req_cfg.get("headers"),
                params=req_cfg.get("params"),
                cookies=req_cfg.get("cookies"),
                json_body=req_cfg.get("json"),
                data=req_cfg.get("data"),
            )
            step_name = step_conf.get("name") if isinstance(step_conf, dict) else None
            step_res = LoginStepResult(index=idx, name=step_name, request=snapshot)
            start_ts = datetime.now(timezone.utc)
            try:
                async with httpx.AsyncClient(timeout=req_cfg.get("timeout", 30.0), follow_redirects=bool(req_cfg.get("allow_redirects", True)), proxy=proxy) as http:
                    resp = await http.request(
                        method=req_cfg["method"],
                        url=req_cfg["url"],
                        headers=req_cfg["headers"],
                        params=req_cfg["params"],
                        json=req_cfg["json"],
                        data=req_cfg["data"],
                        cookies=req_cfg.get("cookies"),
                    )
            except Exception as e:
                end_ts = datetime.now(timezone.utc)
                step_res.durationMs = int((end_ts - start_ts).total_seconds() * 1000)
                step_res.ok = False
                step_res.error = str(e)
                log_login_failure(str(e))
                exec_result.steps.append(step_res)
                return None
            end_ts = datetime.now(timezone.utc)
            step_res.durationMs = int((end_ts - start_ts).total_seconds() * 1000)
            step_res.statusCode = resp.status_code
            step_res.ok = 200 <= resp.status_code < 400
            log_login_step(
                idx,
                step_name or f"step_{idx}",
                req_cfg.get("method"),
                req_cfg.get("url"),
                resp.status_code,
                step_res.durationMs,
            )

            # 解析步骤级 mapping
            step_mapping = step_conf.get("responseMapping") if isinstance(step_conf, dict) else None
            if step_mapping:
                body_json = None
                try:
                    body_json = resp.json()
                except Exception:
                    pass
                headers_dict = _collect_response_headers(resp)
                cookies_dict = {k: v for k, v in resp.cookies.items()}
                raw_body = resp.text
                parsed = await self._parse_response_internal(step_mapping, body_json, headers_dict, cookies_dict, raw_body, auth_method)
                step_res.extracted = parsed.extracted
                step_res.fieldSources = parsed.fieldSources
                effective_payload.update(parsed.extracted)
                log_step(
                    "debug",
                    "login.step.parsed",
                    step_index=idx,
                    keys=list(parsed.extracted.keys()),
                    expires_at=parsed.expiresAt.isoformat() if parsed.expiresAt else None,
                )
            exec_result.steps.append(step_res)
            return resp

        try:
            if steps_conf and isinstance(steps_conf, list):
                for idx, sconf in enumerate(steps_conf):
                    if not isinstance(sconf, dict):
                        continue
                    resp = await _run_one(idx, sconf)
                    if resp is not None:
                        final_response = resp
            else:
                if not flow_config:
                    raise ValueError("未配置登录流程")
                resp = await _run_one(0, flow_config)
                if resp is not None:
                    final_response = resp
        except Exception as e:
            # 保护性：任何未捕获错误都当作失败记录
            log_login_failure(str(e))
            await self.client.secret.update(
                where={"id": secret_id},
                data={
                    "isValid": False,
                    "lastLoginAt": datetime.now(timezone.utc),
                    "lastLoginStatus": "failed",
                    "lastLoginError": str(e),
                    "status": "invalid",
                },
            )
            exec_result.success = False
            exec_result.error = str(e)
            raise

        extracted_data: Dict[str, Any] = {}
        field_sources: Dict[str, str] = {}
        expires_at: Optional[datetime] = None
        if auth_method.responseMapping and final_response is not None:
            body_json = None
            try:
                body_json = final_response.json()
            except Exception:
                pass
            headers_dict = _collect_response_headers(final_response)
            cookies_dict = {k: v for k, v in final_response.cookies.items()}
            raw_body = final_response.text
            parsed = await self._parse_response_internal(auth_method.responseMapping, body_json, headers_dict, cookies_dict, raw_body, auth_method)
            extracted_data.update(parsed.extracted)
            field_sources.update(parsed.fieldSources)
            expires_at = parsed.expiresAt
            exec_result.finalExtracted = extracted_data
            exec_result.finalFieldSources = field_sources
            exec_result.finalExpiresAt = expires_at
            log_login_final(list(extracted_data.keys()), expires_at.isoformat() if expires_at else None)

        update_data: Dict[str, Any] = {
            "data": _to_prisma_json(extracted_data),
            "authDataSources": _to_prisma_json(field_sources),
            "lastRefreshed": datetime.now(timezone.utc),
            "status": "active",
            "isValid": True,
            "lastLoginAt": datetime.now(timezone.utc),
            "lastLoginStatus": "success",
            "lastLoginError": None,
        }
        if expires_at:
            update_data["expiresAt"] = expires_at
        if store_login_payload:
            update_data["loginPayload"] = _to_prisma_json(effective_payload_raw)

        secret_updated = await self.client.secret.update(where={"id": secret_id}, data=update_data)
        exec_result.success = True
        return secret_updated, exec_result
