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, List, Union, Generator, Tuple
from dataclasses import dataclass

from turbo_agent_auth.config.runtime import get_callback_url, get_ip_allow_list

PLACEHOLDER_PATTERN = re.compile(r"\{\{\s*([^}]+)\s*\}\}")

@dataclass
class ResponseData:
    status_code: int
    headers: Dict[str, Any]
    cookies: Dict[str, str]
    text: str
    json_data: Any = None
    is_success: bool = False
    duration_ms: int = 0

@dataclass
class LoginStepResult:
    index: int
    name: str
    request: Dict[str, Any]
    statusCode: Optional[int]
    ok: bool
    durationMs: int
    error: Optional[str] = None
    extracted: Optional[Dict[str, Any]] = None
    fieldSources: Optional[Dict[str, str]] = None

@dataclass
class LoginExecutionResult:
    steps: List[LoginStepResult]
    finalExtracted: Dict[str, Any]
    finalFieldSources: Dict[str, str]
    finalExpiresAt: Optional[datetime]
    success: bool
    error: Optional[str] = None

@dataclass
class ParseResponseResult:
    extracted: Dict[str, Any]
    fieldSources: Dict[str, str]
    expiresAt: Optional[datetime]

def _to_int(value: Any, default: int = 0) -> int:
    try:
        return int(value)
    except (TypeError, ValueError):
        return default

def _extract_path(obj: Dict[str, Any], path: str) -> Any:
    if not path:
        return None
    cur: Any = obj
    for part in path.split('.'):
        if isinstance(cur, dict) and part in cur:
            cur = cur[part]
        else:
            return None
    return cur

def _merge_field_section(target: Dict[str, Any], section: Any) -> None:
    if not isinstance(section, dict):
        return
    for container_key in ("fields", "paths", "map"):
        maybe_fields = section.get(container_key)
        if isinstance(maybe_fields, dict):
            for k, v in maybe_fields.items():
                target[str(k)] = v

def _resolve_body_path(conf: Any) -> Optional[str]:
    if isinstance(conf, str):
        return conf
    if isinstance(conf, dict):
        path = conf.get("path") or conf.get("jsonPath")
        if isinstance(path, str):
            return path
    return None

def _resolve_body_source(conf: Any, default: str) -> str:
    if isinstance(conf, dict) and conf.get("source"):
        return str(conf["source"])
    return default

def _resolve_header_conf(conf: Any) -> tuple[Optional[str], int, Optional[str]]:
    if isinstance(conf, str):
        return conf, 0, None
    if isinstance(conf, dict):
        name = conf.get("name") or conf.get("header") or conf.get("key")
        index = _to_int(conf.get("index", 0), 0)
        source = conf.get("source")
        return (str(name) if name else None, index, str(source) if source else None)
    return None, 0, None

def _resolve_cookie_conf(conf: Any) -> tuple[Optional[str], Optional[str]]:
    if isinstance(conf, str):
        return conf, None
    if isinstance(conf, dict):
        name = conf.get("name") or conf.get("cookie") or conf.get("key")
        source = conf.get("source")
        return (str(name) if name else None, str(source) if source else None)
    return None, None

class _HTMLFieldExtractor(HTMLParser):
    def __init__(self, tag: Optional[str], attrs: Dict[str, str], attribute: Optional[str], collect_text: bool, trim: bool, index: int):
        super().__init__()
        self._tag = tag
        self._attrs = attrs
        self._attribute = attribute
        self._collect_text = collect_text
        self._trim = trim
        self._target_index = max(index, 0)
        self._matched_count = 0
        self._capturing = False
        self._buffer: list[str] = []
        self.value: Optional[str] = None

    def handle_starttag(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None:
        self._handle_start(tag, attrs)

    def handle_startendtag(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None:
        self._handle_start(tag, attrs)

    def _handle_start(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None:
        if self.value is not None:
            return
        if self._tag and tag != self._tag:
            return
        attrs_dict = {k: v for k, v in attrs}
        for expected_key, expected_value in self._attrs.items():
            if attrs_dict.get(expected_key) != expected_value:
                return
        if self._matched_count != self._target_index:
            self._matched_count += 1
            return
        if self._attribute:
            self.value = attrs_dict.get(self._attribute)
        if (not self.value or self._collect_text) and self._collect_text:
            self._capturing = True
            self._buffer.clear()
        self._matched_count += 1

    def handle_data(self, data: str) -> None:
        if self._capturing:
            self._buffer.append(data)

    def handle_endtag(self, tag: str) -> None:
        if not self._capturing:
            return
        if self._tag and tag != self._tag:
            return
        text = ''.join(self._buffer)
        if self._trim:
            text = text.strip()
        if text:
            self.value = text
        self._capturing = False

def _regex_search(text: str, pattern: str, flags: Any, group: Any) -> Optional[str]:
    if not pattern:
        return None
    compiled_flags = 0
    if isinstance(flags, str):
        for ch in flags:
            if ch == 'i':
                compiled_flags |= re.IGNORECASE
            elif ch == 'm':
                compiled_flags |= re.MULTILINE
            elif ch == 's':
                compiled_flags |= re.DOTALL
    elif isinstance(flags, int):
        compiled_flags = flags
    match = re.search(pattern, text, compiled_flags)
    if not match:
        return None
    if group is None:
        if match.groups():
            return match.group(1)
        return match.group(0)
    try:
        return match.group(group)
    except Exception:
        return None

def _extract_html_field(raw_text: str, conf: Any) -> Optional[str]:
    if not raw_text or conf is None:
        return None
    if isinstance(conf, str):
        return _regex_search(raw_text, conf, None, None)
    if isinstance(conf, dict):
        if conf.get("regex"):
            return _regex_search(raw_text, str(conf.get("regex")), conf.get("flags"), conf.get("group"))
        selector = conf.get("selector")
        if isinstance(selector, dict):
            tag = selector.get("tag")
            attrs_conf = selector.get("attrs") or {}
            attrs_map = {str(k): str(v) for k, v in attrs_conf.items() if v is not None}
            attribute = conf.get("attribute")
            collect_text = bool(conf.get("text")) or not attribute
            trim = conf.get("trim", True)
            index = _to_int(conf.get("index", 0), 0)
            parser = _HTMLFieldExtractor(str(tag) if tag else None, attrs_map, str(attribute) if attribute else None, collect_text, bool(trim), index)
            parser.feed(raw_text)
            return parser.value
    return None

def _merge_cookies(headers_in: Dict[str, Any], cookies_dict: Dict[str, Any]) -> tuple[Dict[str, str], Dict[str, str], Optional[datetime]]:
    merged: Dict[str, str] = {}
    source_map: Dict[str, str] = {}
    expiry_candidate: Optional[datetime] = None

    for key, value in (cookies_dict or {}).items():
        if value is None:
            continue
        name = str(key)
        merged[name] = str(value)
        source_map[name] = "cookies"

    header_values: list[str] = []
    for key, value in (headers_in or {}).items():
        if str(key).lower() != "set-cookie":
            continue
        if isinstance(value, list):
            header_values.extend(str(v) for v in value)
        else:
            header_values.append(str(value))

    for raw_header in header_values:
        cookie = SimpleCookie()
        try:
            cookie.load(raw_header)
        except Exception:
            continue
        for morsel in cookie.values():
            name = morsel.key
            merged[name] = morsel.value
            source_map[name] = "headers.set-cookie"
            candidate: Optional[datetime] = None
            max_age = morsel["max-age"]
            expires_attr = morsel["expires"]
            if max_age:
                try:
                    candidate = datetime.now(timezone.utc) + timedelta(seconds=int(max_age))
                except ValueError:
                    candidate = None
            elif expires_attr:
                try:
                    dt = parsedate_to_datetime(expires_attr)
                    if dt.tzinfo is None:
                        dt = dt.replace(tzinfo=timezone.utc)
                    candidate = dt.astimezone(timezone.utc)
                except (TypeError, ValueError):
                    candidate = None
            if candidate and (expiry_candidate is None or candidate < expiry_candidate):
                expiry_candidate = candidate

    return merged, source_map, expiry_candidate

def _resolve_context_value(context: Dict[str, Any], path: str) -> Any:
    current: Any = context
    for part in path.split('.'):
        if isinstance(current, dict) and part in current:
            current = current[part]
        else:
            return None
    return current

def _render_template(value: Any, context: Dict[str, Any]) -> Any:
    if isinstance(value, dict):
        return {k: _render_template(v, context) for k, v in value.items()}
    if isinstance(value, list):
        return [_render_template(v, context) for v in value]
    if isinstance(value, str):
        matches = list(PLACEHOLDER_PATTERN.finditer(value))
        if not matches:
            return value
        if len(matches) == 1 and matches[0].span() == (0, len(value)):
            resolved = _resolve_context_value(context, matches[0].group(1).strip())
            return resolved

        def replacer(match: re.Match[str]) -> str:
            resolved = _resolve_context_value(context, match.group(1).strip())
            if resolved is None:
                return ""
            if isinstance(resolved, (dict, list)):
                return json.dumps(resolved)
            return str(resolved)

        return PLACEHOLDER_PATTERN.sub(replacer, value)
    return value

def _apply_runtime_defaults(payload: Dict[str, Any] | None) -> Dict[str, Any]:
    base: Dict[str, Any] = deepcopy(payload) if payload else {}
    callback_url = get_callback_url()
    if callback_url:
        base.setdefault("callback_url", callback_url)
        base.setdefault("redirect_uri", callback_url)
    return base


def _build_flow_context(payload: Dict[str, Any] | None) -> Dict[str, Any]:
    context = _apply_runtime_defaults(payload)
    context.setdefault("runtime", {})
    runtime_ctx = context["runtime"]
    if isinstance(runtime_ctx, dict):
        runtime_ctx.setdefault("callback_url", get_callback_url())
        runtime_ctx.setdefault("ip_allow_list", get_ip_allow_list())
    else:
        context["runtime"] = {
            "callback_url": get_callback_url(),
            "ip_allow_list": get_ip_allow_list(),
        }
    return context

def prepare_login_request_config(flow: Dict[str, Any], payload: Dict[str, Any]) -> Dict[str, Any]:
    context = _build_flow_context(payload)
    url_template = flow.get("url")
    if not url_template:
        raise ValueError("登录流程未配置 url")
    url_rendered = _render_template(url_template, context) if isinstance(url_template, (dict, list, str)) else url_template
    if not isinstance(url_rendered, str):
        raise ValueError("登录流程 url 模板解析结果必须为字符串")
    method = str(flow.get("method", "POST")).upper()
    headers_template = flow.get("headers") or {}
    headers = _render_template(headers_template, context) if headers_template else {}
    if headers and not isinstance(headers, dict):
        raise ValueError("登录流程 headers 模板解析结果必须为对象")
    headers = {str(k): str(v) for k, v in headers.items() if v is not None}

    query_template = flow.get("query") or flow.get("query_template") or {}
    params = _render_template(query_template, context) if query_template else None
    if params is not None and not isinstance(params, dict):
        raise ValueError("登录流程 query 模板解析结果必须为对象")

    body_template = (
        flow.get("body_template")
        or flow.get("bodyTemplate")
        or flow.get("body")
    )
    json_body: Any = None
    data_body: Any = None
    files_body: Any = None

    content_type = next((headers[k] for k in headers if k.lower() == "content-type"), "")
    if body_template is not None:
        rendered_body = _render_template(body_template, context)
        if isinstance(rendered_body, (dict, list)):
            if "application/x-www-form-urlencoded" in content_type.lower():
                if not isinstance(rendered_body, dict):
                    raise ValueError("表单 Content-Type 需要字典类型 body")
                data_body = {str(k): "" if v is None else str(v) for k, v in rendered_body.items()}
            elif "multipart/form-data" in content_type.lower():
                if not isinstance(rendered_body, dict):
                    raise ValueError("Multipart Content-Type 需要字典类型 body")
                # httpx files format for fields: {'name': (None, 'value')}
                files_body = {str(k): (None, str(v) if v is not None else "") for k, v in rendered_body.items()}
            elif "application/json" in content_type.lower() or not content_type:
                json_body = rendered_body
            else:
                json_body = rendered_body
        elif isinstance(rendered_body, str):
            if "application/json" in content_type.lower():
                try:
                    json_body = json.loads(rendered_body)
                except json.JSONDecodeError:
                    data_body = rendered_body
            else:
                data_body = rendered_body
        else:
            data_body = rendered_body

    timeout_seconds = flow.get("timeoutSeconds") or flow.get("timeout") or 30
    try:
        timeout = float(timeout_seconds)
    except (TypeError, ValueError):
        timeout = 30.0

    allow_redirects = flow.get("allowRedirects")
    if allow_redirects is None:
        allow_redirects = bool(flow.get("allow_redirects", True))

    cookies_template = flow.get("cookies")
    cookies = _render_template(cookies_template, context) if cookies_template else None
    if cookies and not isinstance(cookies, dict):
        cookies = None

    # requestPlacements: headers/query/cookies/body -> sourceField path or inline value
    placements = flow.get("requestPlacements") or flow.get("request_injection")
    if isinstance(placements, dict):
        def _resolve_value(conf: Any) -> Any:
            # string -> path; object -> sourceField/path/value; else pass-through
            if isinstance(conf, str):
                return _resolve_context_value(context, conf)
            if isinstance(conf, dict):
                if "value" in conf:
                    return conf.get("value")
                src = conf.get("sourceField") or conf.get("path")
                val = _resolve_context_value(context, src) if src else None
                # Optional format template: e.g. "session={{value}}"
                fmt = conf.get("format")
                if fmt is not None:
                    return str(fmt).replace("{{value}}", "" if val is None else str(val))
                return val
            return conf

        # headers
        headers_placement = placements.get("headers")
        if isinstance(headers_placement, dict):
            for k, v in headers_placement.items():
                resolved = _resolve_value(v)
                if resolved is not None:
                    headers[str(k)] = str(resolved)

        # query params
        query_placement = placements.get("query")
        if isinstance(query_placement, dict):
            if params is None:
                params = {}
            for k, v in query_placement.items():
                resolved = _resolve_value(v)
                if resolved is not None:
                    params[str(k)] = resolved

        # cookies
        cookies_placement = placements.get("cookies")
        if isinstance(cookies_placement, dict):
            if cookies is None:
                cookies = {}
            for k, v in cookies_placement.items():
                resolved = _resolve_value(v)
                if resolved is not None:
                    cookies[str(k)] = str(resolved)

        # body
        body_placement = placements.get("body")
        if isinstance(body_placement, dict):
            # choose target body container
            target_is_form = "application/x-www-form-urlencoded" in content_type.lower()
            target_is_multipart = "multipart/form-data" in content_type.lower()
            if target_is_form:
                if data_body is None or not isinstance(data_body, dict):
                    data_body = {}
                for k, v in body_placement.items():
                    resolved = _resolve_value(v)
                    if resolved is not None:
                        data_body[str(k)] = "" if resolved is None else str(resolved)
            elif target_is_multipart:
                if files_body is None:
                    files_body = {}
                for k, v in body_placement.items():
                    resolved = _resolve_value(v)
                    if resolved is not None:
                        files_body[str(k)] = (None, str(resolved))
            else:
                if json_body is None or not isinstance(json_body, dict):
                    json_body = {}
                for k, v in body_placement.items():
                    resolved = _resolve_value(v)
                    if resolved is not None:
                        json_body[str(k)] = resolved

    if files_body is not None:
        # Remove Content-Type header to allow httpx to generate it with boundary
        headers = {k: v for k, v in headers.items() if k.lower() != "content-type"}

    return {
        "method": method,
        "cookies": cookies,
        "url": url_rendered,
        "headers": headers,
        "params": params,
        "json": json_body,
        "data": data_body,
        "files": files_body,
        "timeout": timeout,
        "allow_redirects": allow_redirects,
    }

def parse_response_data(
    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_config: Optional[Dict[str, Any]] = None
) -> ParseResponseResult:
    mapping_dict = mapping if isinstance(mapping, dict) else {}
    extracted: Dict[str, Any] = {}
    field_sources: Dict[str, str] = {}

    def record(key: str, value: Any, source: Optional[str]) -> None:
        if value is None:
            return
        key_str = str(key)
        extracted[key_str] = value
        if source:
            field_sources[key_str] = source

    body_fields: Dict[str, Any] = {}
    header_fields: Dict[str, Any] = {}
    cookie_fields: Dict[str, Any] = {}
    html_fields: Dict[str, Any] = {}
    cookie_collect_field: Optional[str] = None

    if mapping_dict:
        fields_conf = mapping_dict.get("fields")
        if isinstance(fields_conf, dict):
            source_hint = str(mapping_dict.get("source", "body")).lower()
            if source_hint == "headers":
                header_fields.update(fields_conf)
            elif source_hint == "cookies":
                cookie_fields.update(fields_conf)
            else:
                body_fields.update(fields_conf)

        _merge_field_section(body_fields, mapping_dict.get("body"))
        _merge_field_section(header_fields, mapping_dict.get("headers"))

        cookies_section = mapping_dict.get("cookies")
        if isinstance(cookies_section, dict):
            _merge_field_section(cookie_fields, cookies_section)
            if cookies_section.get("collectAll") or cookies_section.get("includeAll"):
                cookie_collect_field = str(cookies_section.get("targetField") or cookies_section.get("aggregateField") or "cookies")

        html_section = mapping_dict.get("html")
        if isinstance(html_section, dict):
            html_fields_conf = html_section.get("fields")
            if isinstance(html_fields_conf, dict):
                html_fields.update(html_fields_conf)
            regex_conf = html_section.get("regex")
            if isinstance(regex_conf, dict):
                for key, value in regex_conf.items():
                    if isinstance(value, dict):
                        html_fields.setdefault(str(key), value)
                    else:
                        html_fields.setdefault(str(key), {"regex": value})

        regex_root = mapping_dict.get("regex")
        if isinstance(regex_root, dict):
            for key, value in regex_root.items():
                if isinstance(value, dict):
                    html_fields.setdefault(str(key), value)
                else:
                    html_fields.setdefault(str(key), {"regex": value})

        collect_all = mapping_dict.get("collectCookies")
        if isinstance(collect_all, str):
            cookie_collect_field = collect_all
        elif collect_all is True and not cookie_collect_field:
            cookie_collect_field = "cookies"

        reserved = {"source", "fields", "body", "headers", "cookies", "html", "regex", "collectCookies"}
        simple_candidates: Dict[str, Any] = {}
        for key, value in mapping_dict.items():
            if key in reserved:
                continue
            if isinstance(value, (str, dict)):
                simple_candidates[str(key)] = value
        if simple_candidates and not body_fields:
            body_fields.update(simple_candidates)

    headers_lower = {str(k).lower(): v for k, v in headers.items()} if headers else {}
    merged_cookies, cookie_sources, cookie_expiry_candidate = _merge_cookies(headers, cookies)

    for logical_key, conf in body_fields.items():
        path = _resolve_body_path(conf)
        if not path:
            continue
        value = _extract_path(body, path)
        source_label = _resolve_body_source(conf, f"body.{path}")
        record(logical_key, value, source_label)

    for logical_key, conf in header_fields.items():
        header_name, index, override_source = _resolve_header_conf(conf)
        if not header_name:
            continue
        raw_value: Any = headers_lower.get(header_name.lower()) if headers_lower else None
        value: Any = None
        if isinstance(raw_value, list):
            if 0 <= index < len(raw_value):
                value = raw_value[index]
            elif raw_value:
                value = raw_value[0]
        elif raw_value is not None:
            value = raw_value
        source_label = override_source or f"headers.{header_name}"
        record(logical_key, value, source_label)

    for logical_key, conf in cookie_fields.items():
        cookie_name, override_source = _resolve_cookie_conf(conf)
        if not cookie_name:
            continue
        value = merged_cookies.get(cookie_name)
        if value is None:
            continue
        source_label = override_source or cookie_sources.get(cookie_name, f"cookies.{cookie_name}")
        if source_label == "cookies":
            source_label = f"cookies.{cookie_name}"
        record(logical_key, value, source_label)

    if (cookie_collect_field or (auth_method_config and auth_method_config.get("type") == "Cookies" and "cookies" not in extracted)) and merged_cookies:
        target_field = cookie_collect_field or "cookies"
        record(target_field, merged_cookies, "cookies")
        for cookie_name, src in cookie_sources.items():
            field_sources.setdefault(f"cookies.{cookie_name}", src)

    if cookie_expiry_candidate:
        record("cookie_expires_at", cookie_expiry_candidate.isoformat(), "headers.set-cookie")

    if raw_body and html_fields:
        for logical_key, conf in html_fields.items():
            value = _extract_html_field(raw_body, conf)
            if value is None:
                continue
            source_label = "html.selector"
            if isinstance(conf, dict):
                if conf.get("source"):
                    source_label = str(conf["source"])
                elif conf.get("regex"):
                    source_label = "html.regex"
            record(logical_key, value, source_label)

    token_expires_at: Optional[datetime] = None
    token_expiry_source: Optional[str] = None

    if extracted.get('expires_at') is not None:
        val = extracted['expires_at']
        if isinstance(val, (int, float)):
            token_expires_at = datetime.fromtimestamp(val, tz=timezone.utc)
            token_expiry_source = field_sources.get('expires_at', 'body.expires_at')
        elif isinstance(val, str):
            try:
                token_expires_at = datetime.fromisoformat(val.replace('Z', '+00:00'))
                token_expiry_source = field_sources.get('expires_at', 'body.expires_at')
            except Exception:
                token_expires_at = None
    elif extracted.get('expires_in') is not None:
        try:
            val = extracted['expires_in']
            if isinstance(val, (int, float)) and val > 0:
                if val > 1_000_000_000:
                    token_expires_at = datetime.fromtimestamp(int(val), tz=timezone.utc)
                else:
                    token_expires_at = datetime.now(timezone.utc) + timedelta(seconds=int(val))
                token_expiry_source = field_sources.get('expires_in', 'body.expires_in')
            elif isinstance(val, str):
                token_expires_at = datetime.fromisoformat(val.replace('Z', '+00:00'))
                token_expiry_source = field_sources.get('expires_in', 'body.expires_in')
        except Exception:
            token_expires_at = None
    elif auth_method_config and auth_method_config.get("defaultValiditySeconds"):
        token_expires_at = datetime.now(timezone.utc) + timedelta(seconds=int(auth_method_config["defaultValiditySeconds"]))
        token_expiry_source = 'defaultValiditySeconds'

    final_expires_at: Optional[datetime] = None
    final_source: Optional[str] = None

    if token_expires_at and cookie_expiry_candidate:
        if cookie_expiry_candidate <= token_expires_at:
            final_expires_at = cookie_expiry_candidate
            final_source = f"min({token_expiry_source or 'token'}, headers.set-cookie)"
        else:
            final_expires_at = token_expires_at
            final_source = token_expiry_source
    elif token_expires_at:
        final_expires_at = token_expires_at
        final_source = token_expiry_source
    elif cookie_expiry_candidate:
        final_expires_at = cookie_expiry_candidate
        final_source = "headers.set-cookie"

    if final_expires_at:
        record('expires_at', final_expires_at.isoformat(), final_source or field_sources.get('expires_at') or 'default')

    return ParseResponseResult(extracted=extracted, fieldSources=field_sources, expiresAt=final_expires_at)


class AuthFlowExecutor:
    def __init__(self, auth_method_config: Dict[str, Any], login_payload: Dict[str, Any], flow_type: str = "loginFlow"):
        self.auth_method = auth_method_config
        self.login_payload = login_payload
        self.flow_type = flow_type

    def execute(self) -> Generator[Dict[str, Any], ResponseData, LoginExecutionResult]:
        """
        Generator that yields request configurations and expects ResponseData objects.
        Returns LoginExecutionResult upon completion.
        """
        effective_payload = _apply_runtime_defaults(deepcopy(self.login_payload))
        flow_config = self.auth_method.get(self.flow_type) or {}
        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_data: Optional[ResponseData] = None

        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)
                    
                    # Yield request config and wait for response
                    response_data = yield req_cfg
                    final_response_data = response_data
                    
                    step_result = LoginStepResult(
                        index=idx,
                        name=step.get("name") or f"step_{idx}",
                        request=req_cfg, # Snapshot
                        statusCode=response_data.status_code,
                        ok=response_data.is_success,
                        durationMs=response_data.duration_ms,
                        error=None,
                    )

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

                    # Parse step response
                    step_mapping = step.get("responseMapping")
                    if step_mapping:
                        parsed = parse_response_data(
                            step_mapping, 
                            response_data.json_data, 
                            response_data.headers, 
                            response_data.cookies, 
                            response_data.text, 
                            self.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:
                # Single step
                if not flow_config:
                    raise ValueError(f"未配置流程: {self.flow_type}")
                
                req_cfg = prepare_login_request_config(flow_config, effective_payload)
                
                response_data = yield req_cfg
                final_response_data = response_data

                step_result = LoginStepResult(
                    index=0,
                    name=flow_config.get("name") or "single",
                    request=req_cfg,
                    statusCode=response_data.status_code,
                    ok=response_data.is_success,
                    durationMs=response_data.duration_ms,
                    error=None,
                )
                execution_steps.append(step_result)

                if not response_data.is_success:
                    raise ValueError(f"Request failed with status {response_data.status_code}")

            # Final parse
            response_mapping = self.auth_method.get("responseMapping")
            if response_mapping and final_response_data:
                parsed = parse_response_data(
                    response_mapping, 
                    final_response_data.json_data, 
                    final_response_data.headers, 
                    final_response_data.cookies, 
                    final_response_data.text, 
                    self.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),
            )
