from __future__ import annotations

from typing import Any, Callable, Dict, Optional, Tuple, List, Union, Awaitable
from datetime import datetime, timezone

import httpx
import smtplib
import poplib
import ssl
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from pydantic import BaseModel

from turbo_agent_core.schema.external import Platform, AuthMethod, Secret
from turbo_agent_core.schema.enums import AuthType
from turbo_agent_auth.auth_core import AuthFlowExecutor, ResponseData, LoginExecutionResult

import requests
from requests_oauthlib import OAuth1 as RequestsOAuth1

class BaseAuthClient:
    """
    Base class for AuthClient and AsyncAuthClient.
    Handles the logic of resolving auth parameters from Secret/AuthMethod.
    """

    def __init__(self, platform: Platform, auth_method: Optional[AuthMethod] = None, secret: Optional[Secret] = None, base_url: Optional[str] = None):
        self.platform = platform
        self.auth_method = auth_method
        self.secret = secret
        # Priority: Explicit base_url > Platform baseUrl > Empty
        self.base_url = (base_url or (self.platform.endpoint.baseURL if self.platform.endpoint else "") or "").rstrip("/")

    def needs_user_assistance(self) -> bool:
        """
        Check if the current auth state requires user intervention (e.g. missing login payload).
        Returns True if user assistance is needed.
        """
        # If no auth method or no login flow, we assume no login needed or not possible to login
        if not self.auth_method or not self.auth_method.loginFlow:
            return False
        
        # If no secret, we definitely need user to provide something (at least create a secret)
        # But in the context of "needs assistance to login", if we don't have a secret, we can't login.
        if not self.secret:
            return True

        # Check if expired or no credential
        is_expired = False
        if self.secret.expiresAt:
             # Handle timezone awareness: if expiresAt is naive, assume UTC or local? 
             # Core usually uses UTC.
             tz = self.secret.expiresAt.tzinfo or timezone.utc
             now = datetime.now(tz)
             if self.secret.expiresAt <= now:
                 is_expired = True
        
        has_credential = bool(self.secret.credential)
        
        # If we need to login (expired or no creds)
        if is_expired or not has_credential:
            # We need login. Do we have the payload?
            if not self.secret.loginPayload:
                return True
        
        return False

    def prepare_http(self) -> Dict[str, Dict[str, Any]]:
        """
        Return mapping with keys: headers, query, cookies, body, metadata.
        Applies usageMapping (from Secret or AuthMethod) if provided, else infers defaults by type.
        """
        data = (self.secret.credential if self.secret else {}) or {}
        
        if not self.auth_method:
             return {"headers": {}, "query": {}, "cookies": {}, "body": {}, "metadata": {}}

        # Prefer secret's usageMapping (snapshot), fallback to auth_method's definition
        # Core Secret does not have usageMapping, so we rely on AuthMethod
        usage = self.auth_method.authFieldPlacements or {}
        stype = self.auth_method.authType

        if usage:
            return self._apply_usage_mapping(usage, data)
        return self._infer_defaults(stype, data)
        usage = self.auth_method.authFieldPlacements or {}
        stype = self.auth_method.authType

        if usage:
            return self._apply_usage_mapping(usage, data)
        return self._infer_defaults(stype, data)

    def _merge_request_params(self, kwargs: Dict[str, Any], applied: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
        """
        Merge user-provided kwargs with applied auth params.
        """
        # Headers
        headers = kwargs.get("headers") or {}
        if applied.get("headers"):
            headers.update(applied["headers"])
        kwargs["headers"] = headers

        # Query Params
        params = kwargs.get("params") or {}
        if applied.get("query"):
            params.update(applied["query"])
        kwargs["params"] = params

        # Cookies
        cookies = kwargs.get("cookies") or {}
        if applied.get("cookies"):
            cookies.update(applied["cookies"])
        kwargs["cookies"] = cookies

        # Body (JSON/Data) - Simple merge if both are dicts, otherwise applied takes precedence if set?
        # Usually auth body params are specific fields.
        # If user passes json, we try to update it.
        if applied.get("body"):
            if kwargs.get("json") is not None and isinstance(kwargs["json"], dict):
                kwargs["json"].update(applied["body"])
            elif kwargs.get("data") is not None and isinstance(kwargs["data"], dict):
                kwargs["data"].update(applied["body"])
            elif kwargs.get("json") is None and kwargs.get("data") is None:
                # If no body provided, use applied body as json if it looks like data
                kwargs["json"] = applied["body"]
        
        return kwargs

    def _create_response_data(self, response: httpx.Response) -> ResponseData:
        body_json = None
        try:
            body_json = response.json()
        except Exception:
            pass
        
        headers_dict = {}
        for k, v in response.headers.multi_items():
            if k in headers_dict:
                if isinstance(headers_dict[k], list):
                    headers_dict[k].append(v)
                else:
                    headers_dict[k] = [headers_dict[k], v]
            else:
                headers_dict[k] = v
                
        return ResponseData(
            status_code=response.status_code,
            headers=headers_dict,
            cookies={k: v for k, v in response.cookies.items()},
            text=response.text,
            json_data=body_json,
            is_success=response.is_success,
            duration_ms=int(response.elapsed.total_seconds() * 1000)
        )

    # ---------- Internals ----------
    @staticmethod
    def _get_by_path(obj: Any, path: str):
        cur = obj
        for p in path.split('.'):
            if isinstance(cur, dict) and p in cur:
                cur = cur[p]
            else:
                return None
        return cur

    def _resolve_spec(self, spec: Any, data: Dict[str, Any]):
        # Support: str path | {sourceField,format|template,type} | {tokenField,type} | {value}
        if isinstance(spec, str):
            return (self._get_by_path(data, spec)
                    if ('.' in spec or spec in data)
                    else spec)
        if isinstance(spec, dict):
            if 'value' in spec:
                return spec['value']
            field = spec.get('sourceField') or spec.get('tokenField') or spec.get('valueField')
            fmt = spec.get('format') or spec.get('template')
            tok_type = spec.get('type')
            val = self._get_by_path(data, field) if field else None
            if val is None:
                return None
            if tok_type and tok_type.lower() == 'bearer':
                return f"Bearer {val}"
            if fmt:
                return fmt.replace('{{value}}', str(val))
            return val
        return spec

    def _apply_usage_mapping(self, mapping: Dict[str, Any], data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
        result: Dict[str, Dict[str, Any]] = {"headers": {}, "query": {}, "cookies": {}, "body": {}, "metadata": {}}
        for section in list(result.keys()):
            conf = mapping.get(section)
            if isinstance(conf, dict):
                for k, v in conf.items():
                    rv = self._resolve_spec(v, data)
                    if rv is not None:
                        result[section][k] = rv
        return result

    def _infer_defaults(self, stype: Optional[str], data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
        headers: Dict[str, Any] = {}
        query: Dict[str, Any] = {}
        cookies: Dict[str, Any] = {}
        body: Dict[str, Any] = {}
        metadata: Dict[str, Any] = {}

        access_token = data.get('access_token') or data.get('token')
        if stype in ("JWT", "OAuth2") and access_token:
            headers['Authorization'] = f"Bearer {access_token}"
        elif stype == "APIKey":
            api_key = data.get('api_key')
            if api_key:
                headers['X-API-Key'] = api_key
        elif stype == "Cookies":
            if isinstance(data.get('cookies'), dict):
                cookies.update(data['cookies'])
        elif stype == "OAuth1":
            for f in ("consumer_key", "consumer_secret", "oauth_token", "oauth_token_secret", "access_token", "access_token_secret"):
                if f in data:
                    metadata[f] = data[f]
        elif stype in ("SMTP", "POP3"):
            metadata.update(data)
        else:
            # Generic fallback: try Authorization from access_token
            if access_token:
                headers['Authorization'] = f"Bearer {access_token}"
        return {"headers": headers, "query": query, "cookies": cookies, "body": body, "metadata": metadata}

    def get_connection_params(self) -> Dict[str, Any]:
        """
        For non-HTTP types (SMTP/POP3), return connection params based on usageMapping.metadata or data.
        """
        data = (self.secret.credential if self.secret else {}) or {}
        
        if not self.auth_method:
             return {"data": data, "applied": self.prepare_http()}

        usage = self.auth_method.authFieldPlacements or {}
        stype = self.auth_method.authType
        
        meta = (usage.get("metadata") or {}) if usage else {}
        if stype == AuthType.SMTP:
            smtp_meta = meta.get("smtp", {})
            return {
                "host": data.get(smtp_meta.get("hostField", "smtp_host")),
                "port": data.get(smtp_meta.get("portField", "smtp_port"), 587),
                "username": data.get(smtp_meta.get("userField", "username")),
                "password": data.get(smtp_meta.get("passField", "password")),
                "useTLS": smtp_meta.get("useTLS", True),
            }
        if stype == AuthType.POP3:
            pop_meta = meta.get("pop3", {})
            return {
                "host": data.get(pop_meta.get("hostField", "pop3_host")),
                "port": data.get(pop_meta.get("portField", "pop3_port"), 995),
                "username": data.get(pop_meta.get("userField", "username")),
                "password": data.get(pop_meta.get("passField", "password")),
                "useSSL": pop_meta.get("useSSL", True),
            }
        # For other types just return raw data and applied http fields
        return {
            "data": data,
            "applied": self.prepare_http()
        }


class AuthClient(BaseAuthClient):
    """
    Synchronous Auth Client.
    """
    def __init__(self, platform: Platform, auth_method: Optional[AuthMethod] = None, secret: Optional[Secret] = None, base_url: Optional[str] = None, session: Optional[httpx.Client] = None):
        super().__init__(platform, auth_method, secret, base_url)
        self.session = session or httpx.Client(base_url=self.base_url if self.base_url else "")

    def login(self, force: bool = False) -> Optional[Dict[str, Any]]:
        """
        Execute the login flow defined in AuthMethod using the secret's loginPayload.
        Updates the secret's data with the result.
        """
        if not self.auth_method or not self.auth_method.loginFlow:
            return None
        
        if not self.secret:
             # Cannot update secret if it doesn't exist
             return None

        # Check expiration if not forced
        if not force and self.secret.expiresAt:
            tz = self.secret.expiresAt.tzinfo or timezone.utc
            now = datetime.now(tz)
            if self.secret.expiresAt > now:
                # Not expired, no need to login
                return self.secret.credential

        # Check payload
        if not self.secret.loginPayload:
             raise ValueError("Login required but no loginPayload provided. User assistance needed.")
        
        executor = AuthFlowExecutor(self.auth_method.model_dump(), self.secret.loginPayload or {})
        
        def sync_sender(req_cfg: Dict[str, Any]) -> ResponseData:
            resp = self.session.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"),
                timeout=req_cfg.get("timeout"),
                follow_redirects=req_cfg.get("allow_redirects")
            )
            return self._create_response_data(resp)

        gen = executor.execute()
        try:
            req_cfg = next(gen)
            while True:
                resp_data = sync_sender(req_cfg)
                req_cfg = gen.send(resp_data)
        except StopIteration as e:
            result = e.value
            if not result.success:
                raise Exception(f"Login failed: {result.error}")
            
            if self.secret.credential is None:
                self.secret.credential = {}
            if isinstance(self.secret.credential, dict):
                self.secret.credential.update(result.finalExtracted)
            return result.finalExtracted

    def refresh(self) -> Dict[str, Any]:
        """
        Execute the refresh flow defined in AuthMethod using the secret's current data.
        Updates the secret's data with the result.
        """
        if not self.auth_method or not self.secret:
            return {}

        # Refresh flow uses current secret data as payload
        executor = AuthFlowExecutor(self.auth_method.model_dump(), self.secret.credential or {}, flow_type="refreshFlow")
        
        def sync_sender(req_cfg: Dict[str, Any]) -> ResponseData:
            resp = self.session.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"),
                timeout=req_cfg.get("timeout"),
                follow_redirects=req_cfg.get("allow_redirects")
            )
            return self._create_response_data(resp)

        gen = executor.execute()
        try:
            req_cfg = next(gen)
            while True:
                resp_data = sync_sender(req_cfg)
                req_cfg = gen.send(resp_data)
        except StopIteration as e:
            result = e.value
            if not result.success:
                raise Exception(f"Refresh failed: {result.error}")
            if self.secret.credential is None:
                self.secret.credential = {}
            if isinstance(self.secret.credential, dict):
                self.secret.credential.update(result.finalExtracted)
            return result.finalExtracted

    def request(self, method: str, url: str, **kwargs) -> httpx.Response:
        """
        Perform an HTTP request applying authorization from the secret.
        """
        applied = self.prepare_http()
        kwargs = self._merge_request_params(kwargs, applied)
        
        # OAuth1 Handling
        stype = self.auth_method.authType if self.auth_method else None
        oauth1_meta = (applied.get("metadata", {}).get("oauth1") or None)
        
        oauth1_signer = kwargs.pop("oauth1_signer", None)

        if stype == AuthType.OAuth1 and oauth1_meta:
            # Preferred: external signer
            if oauth1_signer is not None:
                extra_headers = oauth1_signer(method.upper(), url, kwargs.get("headers"), kwargs.get("params"), kwargs.get("json") or kwargs.get("data"), oauth1_meta, (self.secret.credential if self.secret else {}) or {})
                if extra_headers:
                    kwargs["headers"].update(extra_headers)
            # Built-in: requests + requests_oauthlib
            else:
                data_dict = (self.secret.credential if self.secret else {}) or {}
                consumer_key = data_dict.get(oauth1_meta.get("consumerKeyField", "consumer_key"))
                consumer_secret = data_dict.get(oauth1_meta.get("consumerSecretField", "consumer_secret"))
                token = data_dict.get(oauth1_meta.get("tokenField", "oauth_token")) or data_dict.get("access_token")
                token_secret = data_dict.get(oauth1_meta.get("tokenSecretField", "oauth_token_secret")) or data_dict.get("access_token_secret")
                sig_method = oauth1_meta.get("signatureMethod", "HMAC-SHA1")
                auth = RequestsOAuth1(
                    client_key=consumer_key,
                    client_secret=consumer_secret,
                    resource_owner_key=token,
                    resource_owner_secret=token_secret,
                    signature_method=sig_method,
                )
                # Use requests for OAuth1 if httpx oauth1 is not set up (httpx-oauth is separate lib)
                # But we want to return httpx.Response if possible, or just requests.Response?
                # The original code returned requests.Response if OAuth1.
                with requests.Session() as rs:  # type: ignore
                    return rs.request(
                        method=method.upper(),
                        url=self.base_url + url if not url.startswith("http") and self.base_url else url,
                        headers=kwargs.get("headers"),
                        params=kwargs.get("params"),
                        cookies=kwargs.get("cookies"),
                        json=kwargs.get("json"),
                        data=kwargs.get("data"),
                        timeout=kwargs.get("timeout"),
                        auth=auth,
                    )

        return self.session.request(method, url, **kwargs)

    def get(self, url: str, **kwargs) -> httpx.Response:
        return self.request("GET", url, **kwargs)

    def post(self, url: str, **kwargs) -> httpx.Response:
        return self.request("POST", url, **kwargs)

    def put(self, url: str, **kwargs) -> httpx.Response:
        return self.request("PUT", url, **kwargs)

    def patch(self, url: str, **kwargs) -> httpx.Response:
        return self.request("PATCH", url, **kwargs)

    def delete(self, url: str, **kwargs) -> httpx.Response:
        return self.request("DELETE", url, **kwargs)

    # ---------- SMTP Utilities ----------
    def smtp_send(
        self,
        from_addr: str,
        to_addrs: List[str] | str,
        *,
        subject: str,
        text: Optional[str] = None,
        html: Optional[str] = None,
        timeout: Optional[float] = 30.0,
    ) -> Dict[str, Any]:
        """
        Send an email via SMTP using connection params from the Secret.
        """
        params = self.get_connection_params()
        host = params.get("host")
        port = params.get("port", 587)
        username = params.get("username")
        password = params.get("password")
        use_tls = params.get("useTLS", True)

        if isinstance(to_addrs, str):
            to_list = [addr.strip() for addr in to_addrs.split(',') if addr.strip()]
        else:
            to_list = to_addrs

        if not host or not username or not password:
            raise ValueError("SMTP connection parameters incomplete (host/username/password).")

        # Build message
        msg: MIMEMultipart | MIMEText
        if html is not None or (html is None and text is None):
            msg = MIMEMultipart('alternative')
            if text:
                msg.attach(MIMEText(text, 'plain', 'utf-8'))
            msg.attach(MIMEText(html or (text or ''), 'html', 'utf-8'))
        else:
            msg = MIMEText(text or '', 'plain', 'utf-8')
        msg['Subject'] = subject
        msg['From'] = from_addr
        msg['To'] = ', '.join(to_list)

        context = ssl.create_default_context()

        if use_tls and int(port) == 465:
            with smtplib.SMTP_SSL(host, port, timeout=timeout, context=context) as server:
                server.login(username, password)
                res = server.sendmail(from_addr, to_list, msg.as_string())
                return {"rejected": res}
        else:
            with smtplib.SMTP(host, port, timeout=timeout) as server:
                server.ehlo()
                if use_tls:
                    server.starttls(context=context)
                    server.ehlo()
                server.login(username, password)
                res = server.sendmail(from_addr, to_list, msg.as_string())
                return {"rejected": res}

    # ---------- POP3 Utilities ----------
    def pop3_list(self) -> List[Dict[str, Any]]:
        """
        List messages on a POP3 mailbox using parameters from the Secret.
        """
        params = self.get_connection_params()
        host = params.get("host")
        port = params.get("port", 995)
        username = params.get("username")
        password = params.get("password")
        use_ssl = params.get("useSSL", True)
        if not host or not username or not password:
            raise ValueError("POP3 connection parameters incomplete (host/username/password).")

        if use_ssl:
            conn = poplib.POP3_SSL(host, port)
        else:
            conn = poplib.POP3(host, port)
        try:
            conn.user(username)
            conn.pass_(password)
            _, items, _ = conn.list()
            # Try UIDL
            uidl_map = {}
            try:
                _, uidl_lines, _ = conn.uidl()
                for line in uidl_lines:
                    parts = line.decode(errors='ignore').split()
                    if len(parts) >= 2:
                        uidl_map[int(parts[0])] = parts[1]
            except Exception:
                pass
            result: List[Dict[str, Any]] = []
            for line in items:
                txt = line.decode(errors='ignore')
                parts = txt.split()
                if len(parts) >= 2:
                    idx = int(parts[0])
                    size = int(parts[1])
                    result.append({"index": idx, "size": size, "uid": uidl_map.get(idx)})
            return result
        finally:
            try:
                conn.quit()
            except Exception:
                conn.close()

    def pop3_fetch(self, index: int) -> str:
        """
        Fetch a message by 1-based index. Returns raw message string.
        """
        params = self.get_connection_params()
        host = params.get("host")
        port = params.get("port", 995)
        username = params.get("username")
        password = params.get("password")
        use_ssl = params.get("useSSL", True)
        if not host or not username or not password:
            raise ValueError("POP3 connection parameters incomplete (host/username/password).")

        if use_ssl:
            conn = poplib.POP3_SSL(host, port)
        else:
            conn = poplib.POP3(host, port)
        try:
            conn.user(username)
            conn.pass_(password)
            _, lines, _ = conn.retr(index)
            return "\n".join(line.decode('utf-8', errors='ignore') for line in lines)
        finally:
            try:
                conn.quit()
            except Exception:
                conn.close()


class AsyncAuthClient(BaseAuthClient):
    """
    Asynchronous Auth Client.
    """
    def __init__(self, platform: Platform, auth_method: Optional[AuthMethod] = None, secret: Optional[Secret] = None, base_url: Optional[str] = None, session: Optional[httpx.AsyncClient] = None):
        super().__init__(platform, auth_method, secret, base_url)
        self.session = session or httpx.AsyncClient(base_url=self.base_url if self.base_url else "")

    async def login(self, force: bool = False) -> Optional[Dict[str, Any]]:
        """
        Execute the login flow defined in AuthMethod using the secret's loginPayload.
        Updates the secret's data with the result.
        """
        if not self.auth_method or not self.auth_method.loginFlow:
            return None
        
        if not self.secret:
             # Cannot update secret if it doesn't exist
             return None

        # Check expiration if not forced
        if not force and self.secret.expiresAt:
            tz = self.secret.expiresAt.tzinfo or timezone.utc
            now = datetime.now(tz)
            if self.secret.expiresAt > now:
                # Not expired, no need to login
                return self.secret.credential

        # Check payload
        if not self.secret.loginPayload:
             raise ValueError("Login required but no loginPayload provided. User assistance needed.")
        
        executor = AuthFlowExecutor(self.auth_method.model_dump(), self.secret.loginPayload or {})
        
        async def async_sender(req_cfg: Dict[str, Any]) -> ResponseData:
            resp = await self.session.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"),
                timeout=req_cfg.get("timeout"),
                follow_redirects=req_cfg.get("allow_redirects")
            )
            return self._create_response_data(resp)

        gen = executor.execute()
        try:
            req_cfg = next(gen)
            while True:
                resp_data = await async_sender(req_cfg)
                req_cfg = gen.send(resp_data)
        except StopIteration as e:
            result = e.value
            if not result.success:
                raise Exception(f"Login failed: {result.error}")
            
            if self.secret.credential is None:
                self.secret.credential = {}
            if isinstance(self.secret.credential, dict):
                self.secret.credential.update(result.finalExtracted)
            return result.finalExtracted

    async def refresh(self) -> Dict[str, Any]:
        """
        Execute the refresh flow defined in AuthMethod using the secret's current data.
        Updates the secret's data with the result.
        """
        if not self.auth_method or not self.secret:
            return {}
        executor = AuthFlowExecutor(self.auth_method.model_dump(), self.secret.credential or {}, flow_type="refreshFlow") 
        async def async_sender(req_cfg: Dict[str, Any]) -> ResponseData:
            resp = await self.session.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"),
                timeout=req_cfg.get("timeout"),
                follow_redirects=req_cfg.get("allow_redirects")
            )
            return self._create_response_data(resp)

        gen = executor.execute()
        try:
            req_cfg = next(gen)
            while True:
                resp_data = await async_sender(req_cfg)
                req_cfg = gen.send(resp_data)
        except StopIteration as e:
            result = e.value
            if not result.success:
                raise Exception(f"Refresh failed: {result.error}")
            
            if self.secret.credential is None:
                self.secret.credential = {}
            if isinstance(self.secret.credential, dict):
                self.secret.credential.update(result.finalExtracted)
            return result.finalExtracted

    async def request(self, method: str, url: str, **kwargs) -> httpx.Response:
        """
        Perform an HTTP request applying authorization from the secret.
        """
        applied = self.prepare_http()
        kwargs = self._merge_request_params(kwargs, applied)
        
        # OAuth1 Handling (Async not supported by requests-oauthlib, and httpx-oauth is not in deps)
        # If OAuth1 is needed in async, user must provide a signer that returns headers.
        stype = self.auth_method.authType if self.auth_method else None
        oauth1_meta = (applied.get("metadata", {}).get("oauth1") or None)
        
        oauth1_signer = kwargs.pop("oauth1_signer", None)
        if stype == AuthType.OAuth1 and oauth1_meta:
            if oauth1_signer is not None:
                extra_headers = oauth1_signer(method.upper(), url, kwargs.get("headers"), kwargs.get("params"), kwargs.get("json") or kwargs.get("data"), oauth1_meta, (self.secret.credential if self.secret else {}) or {})
                if extra_headers:
                    kwargs["headers"].update(extra_headers)
            else:
                # Fallback or error?
                # For now, we can't do async OAuth1 without a signer.
                pass

        return await self.session.request(method, url, **kwargs)

    async def get(self, url: str, **kwargs) -> httpx.Response:
        return await self.request("GET", url, **kwargs)

    async def post(self, url: str, **kwargs) -> httpx.Response:
        return await self.request("POST", url, **kwargs)

    async def put(self, url: str, **kwargs) -> httpx.Response:
        return await self.request("PUT", url, **kwargs)

    async def patch(self, url: str, **kwargs) -> httpx.Response:
        return await self.request("PATCH", url, **kwargs)

    async def delete(self, url: str, **kwargs) -> httpx.Response:
        return await self.request("DELETE", url, **kwargs)


    # ---------- SMTP Utilities ----------
    def smtp_send(
        self,
        from_addr: str,
        to_addrs: List[str] | str,
        *,
        subject: str,
        text: Optional[str] = None,
        html: Optional[str] = None,
        timeout: Optional[float] = 30.0,
    ) -> Dict[str, Any]:
        """
        Send an email via SMTP using connection params from the Secret.
        """
        params = self.get_connection_params()
        host = params.get("host")
        port = params.get("port", 587)
        username = params.get("username")
        password = params.get("password")
        use_tls = params.get("useTLS", True)

        if isinstance(to_addrs, str):
            to_list = [addr.strip() for addr in to_addrs.split(',') if addr.strip()]
        else:
            to_list = to_addrs

        if not host or not username or not password:
            raise ValueError("SMTP connection parameters incomplete (host/username/password).")

        # Build message
        msg: MIMEMultipart | MIMEText
        if html is not None or (html is None and text is None):
            msg = MIMEMultipart('alternative')
            if text:
                msg.attach(MIMEText(text, 'plain', 'utf-8'))
            msg.attach(MIMEText(html or (text or ''), 'html', 'utf-8'))
        else:
            msg = MIMEText(text or '', 'plain', 'utf-8')
        msg['Subject'] = subject
        msg['From'] = from_addr
        msg['To'] = ', '.join(to_list)

        context = ssl.create_default_context()

        if use_tls and int(port) == 465:
            with smtplib.SMTP_SSL(host, port, timeout=timeout, context=context) as server:
                server.login(username, password)
                res = server.sendmail(from_addr, to_list, msg.as_string())
                return {"rejected": res}
        else:
            with smtplib.SMTP(host, port, timeout=timeout) as server:
                server.ehlo()
                if use_tls:
                    server.starttls(context=context)
                    server.ehlo()
                server.login(username, password)
                res = server.sendmail(from_addr, to_list, msg.as_string())
                return {"rejected": res}

    # ---------- POP3 Utilities ----------
    def pop3_list(self) -> List[Dict[str, Any]]:
        """
        List messages on a POP3 mailbox using parameters from the Secret.
        """
        params = self.get_connection_params()
        host = params.get("host")
        port = params.get("port", 995)
        username = params.get("username")
        password = params.get("password")
        use_ssl = params.get("useSSL", True)
        if not host or not username or not password:
            raise ValueError("POP3 connection parameters incomplete (host/username/password).")

        if use_ssl:
            conn = poplib.POP3_SSL(host, port)
        else:
            conn = poplib.POP3(host, port)
        try:
            conn.user(username)
            conn.pass_(password)
            _, items, _ = conn.list()
            # Try UIDL
            uidl_map = {}
            try:
                _, uidl_lines, _ = conn.uidl()
                for line in uidl_lines:
                    parts = line.decode(errors='ignore').split()
                    if len(parts) >= 2:
                        uidl_map[int(parts[0])] = parts[1]
            except Exception:
                pass
            result: List[Dict[str, Any]] = []
            for line in items:
                txt = line.decode(errors='ignore')
                parts = txt.split()
                if len(parts) >= 2:
                    idx = int(parts[0])
                    size = int(parts[1])
                    result.append({"index": idx, "size": size, "uid": uidl_map.get(idx)})
            return result
        finally:
            try:
                conn.quit()
            except Exception:
                conn.close()

    def pop3_fetch(self, index: int) -> str:
        """
        Fetch a message by 1-based index. Returns raw message string.
        """
        params = self.get_connection_params()
        host = params.get("host")
        port = params.get("port", 995)
        username = params.get("username")
        password = params.get("password")
        use_ssl = params.get("useSSL", True)
        if not host or not username or not password:
            raise ValueError("POP3 connection parameters incomplete (host/username/password).")

        if use_ssl:
            conn = poplib.POP3_SSL(host, port)
        else:
            conn = poplib.POP3(host, port)
        try:
            conn.user(username)
            conn.pass_(password)
            _, lines, _ = conn.retr(index)
            return "\n".join(line.decode('utf-8', errors='ignore') for line in lines)
        finally:
            try:
                conn.quit()
            except Exception:
                conn.close()



