import asyncio
import hashlib
import hmac
import logging
import re
import secrets
import smtplib
import ssl
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from email.message import EmailMessage
from email.utils import formataddr
from typing import Optional

from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession

from bot.middlewares.i18n import JsonI18n
from bot.services.email_templates import EmailContent, render_login_code
from bot.services.message_audit import log_user_message_delivery
from config.settings import Settings
from db.dal import security_dal, user_dal
from db.models import EmailVerificationCode

logger = logging.getLogger(__name__)

EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")


@dataclass(frozen=True)
class SmtpAttempt:
    port: int
    use_ssl: bool
    starttls: bool


@dataclass(frozen=True)
class EmailCodeRequestResult:
    ok: bool
    error: Optional[str] = None
    retry_after: Optional[int] = None


@dataclass(frozen=True)
class EmailCodeVerifyResult:
    ok: bool
    error: Optional[str] = None
    retry_after: Optional[int] = None


@dataclass(frozen=True)
class EmailMagicVerifyResult:
    ok: bool
    error: Optional[str] = None
    email: Optional[str] = None
    purpose: Optional[str] = None
    target_user_id: Optional[int] = None


def normalize_email(value: str) -> str:
    return (value or "").strip().lower()


def is_valid_email(value: str) -> bool:
    email = normalize_email(value)
    return bool(email and len(email) <= 254 and EMAIL_RE.match(email))


def _email_throttle_identifier(email: str, purpose: str, target_user_id: Optional[int]) -> str:
    target_part = "none" if target_user_id is None else str(target_user_id)
    return f"{purpose}:{target_part}:{email}"


class EmailAuthService:
    def __init__(self, settings: Settings, i18n: Optional[JsonI18n] = None):
        self.settings = settings
        self.i18n = i18n

    def _smtp_attempts(self) -> list[SmtpAttempt]:
        attempts: list[SmtpAttempt] = []
        primary_port = int(self.settings.SMTP_PORT)

        for port in self.settings.smtp_ports_to_try:
            if port == primary_port:
                use_ssl = bool(self.settings.SMTP_USE_SSL or port == 465)
                starttls = bool(self.settings.SMTP_STARTTLS and not use_ssl)
            else:
                use_ssl = port == 465
                starttls = bool(self.settings.SMTP_STARTTLS and not use_ssl)
            attempts.append(SmtpAttempt(port=port, use_ssl=use_ssl, starttls=starttls))

        return attempts or [
            SmtpAttempt(
                port=primary_port,
                use_ssl=bool(self.settings.SMTP_USE_SSL or primary_port == 465),
                starttls=bool(
                    self.settings.SMTP_STARTTLS
                    and not self.settings.SMTP_USE_SSL
                    and primary_port != 465
                ),
            )
        ]

    def _hash_code(self, email: str, purpose: str, code: str) -> str:
        secret = hmac.new(
            self.settings.BOT_TOKEN.encode("utf-8"),
            b"remnawave-tg-shop-email-code",
            hashlib.sha256,
        ).digest()
        payload = f"{purpose}:{email}:{code}".encode("utf-8")
        return hmac.new(secret, payload, hashlib.sha256).hexdigest()

    def _hash_magic_token(self, token: str) -> str:
        secret = hmac.new(
            self.settings.BOT_TOKEN.encode("utf-8"),
            b"remnawave-tg-shop-email-magic",
            hashlib.sha256,
        ).digest()
        return hmac.new(secret, token.encode("utf-8"), hashlib.sha256).hexdigest()

    def _build_magic_link(self, *, token: str, purpose: str) -> Optional[str]:
        base_url = (self.settings.SUBSCRIPTION_MINI_APP_URL or "").strip()
        if not base_url:
            return None
        from urllib.parse import urlencode, urlsplit, urlunsplit

        parsed = urlsplit(base_url)
        if parsed.scheme not in ("http", "https") or not parsed.netloc:
            return None
        params = {"login_token": token}
        if purpose and purpose != "login":
            params["login_purpose"] = purpose
        existing_query = parsed.query
        new_query = urlencode(params)
        merged_query = f"{existing_query}&{new_query}" if existing_query else new_query
        return urlunsplit(
            (parsed.scheme, parsed.netloc, parsed.path, merged_query, parsed.fragment)
        )

    async def request_code(
        self,
        session: AsyncSession,
        *,
        email: str,
        purpose: str,
        language_code: str,
        target_user_id: Optional[int] = None,
    ) -> EmailCodeRequestResult:
        normalized_email = normalize_email(email)
        if not self.settings.email_auth_configured:
            return EmailCodeRequestResult(ok=False, error="email_auth_not_configured")
        if not is_valid_email(normalized_email):
            return EmailCodeRequestResult(ok=False, error="invalid_email")

        now = datetime.now(timezone.utc)
        throttle = await security_dal.check_throttle(
            session,
            scope=security_dal.EMAIL_CODE_VERIFY_SCOPE,
            identifier=_email_throttle_identifier(normalized_email, purpose, target_user_id),
            now=now,
        )
        if throttle.locked:
            return EmailCodeRequestResult(
                ok=False,
                error="rate_limited",
                retry_after=throttle.retry_after,
            )

        latest_code = await self._get_latest_code(
            session,
            email=normalized_email,
            purpose=purpose,
            target_user_id=target_user_id,
        )
        if latest_code and latest_code.created_at:
            created_at = latest_code.created_at
            if created_at.tzinfo is None:
                created_at = created_at.replace(tzinfo=timezone.utc)
            resend_after = max(1, int(self.settings.EMAIL_CODE_RESEND_SECONDS))
            elapsed = int((now - created_at).total_seconds())
            if elapsed < resend_after and latest_code.consumed_at is None:
                return EmailCodeRequestResult(
                    ok=False,
                    error="rate_limited",
                    retry_after=resend_after - elapsed,
                )

        await session.execute(
            update(EmailVerificationCode)
            .where(
                EmailVerificationCode.email == normalized_email,
                EmailVerificationCode.purpose == purpose,
                EmailVerificationCode.target_user_id == target_user_id,
                EmailVerificationCode.status == "active",
                EmailVerificationCode.consumed_at.is_(None),
            )
            .values(status="superseded")
        )

        code = f"{secrets.randbelow(1_000_000):06d}"
        magic_token = secrets.token_urlsafe(32)
        magic_link = (
            self._build_magic_link(token=magic_token, purpose=purpose)
            if purpose == "login"
            else None
        )
        code_model = EmailVerificationCode(
            email=normalized_email,
            code_hash=self._hash_code(normalized_email, purpose, code),
            magic_token_hash=self._hash_magic_token(magic_token) if magic_link else None,
            purpose=purpose,
            target_user_id=target_user_id,
            expires_at=now + timedelta(seconds=max(60, int(self.settings.EMAIL_CODE_TTL_SECONDS))),
            status="active",
        )
        session.add(code_model)
        await session.flush()

        await self._send_code_email(
            email=normalized_email,
            code=code,
            language_code=language_code,
            magic_link=magic_link,
            purpose=purpose,
        )
        resolved_target_user_id = target_user_id
        if resolved_target_user_id is None:
            try:
                existing_user = await user_dal.get_user_by_email(session, normalized_email)
                resolved_target_user_id = (
                    int(existing_user.user_id) if existing_user is not None else None
                )
            except Exception:
                logger.exception(
                    "Failed to resolve email auth target user for audit log: %s",
                    normalized_email,
                )
        await log_user_message_delivery(
            session,
            target_user_id=resolved_target_user_id,
            event_type="email_login_code_sent"
            if purpose == "login"
            else "email_verification_code_sent",
            channel="email",
            recipient=normalized_email,
            content=f"purpose={purpose} magic_link={bool(magic_link)}",
        )
        return EmailCodeRequestResult(ok=True)

    async def verify_code(
        self,
        session: AsyncSession,
        *,
        email: str,
        purpose: str,
        code: str,
        target_user_id: Optional[int] = None,
    ) -> EmailCodeVerifyResult:
        normalized_email = normalize_email(email)
        normalized_code = re.sub(r"\D", "", code or "")
        if not is_valid_email(normalized_email):
            return EmailCodeVerifyResult(ok=False, error="invalid_code")

        now = datetime.now(timezone.utc)
        throttle_identifier = _email_throttle_identifier(
            normalized_email,
            purpose,
            target_user_id,
        )
        throttle = await security_dal.check_throttle(
            session,
            scope=security_dal.EMAIL_CODE_VERIFY_SCOPE,
            identifier=throttle_identifier,
            now=now,
        )
        if throttle.locked:
            return EmailCodeVerifyResult(
                ok=False,
                error="rate_limited",
                retry_after=throttle.retry_after,
            )

        latest_code = await self._get_latest_code(
            session,
            email=normalized_email,
            purpose=purpose,
            target_user_id=target_user_id,
        )
        if not latest_code or latest_code.consumed_at is not None:
            return EmailCodeVerifyResult(ok=False, error="invalid_code")

        expires_at = latest_code.expires_at
        if expires_at.tzinfo is None:
            expires_at = expires_at.replace(tzinfo=timezone.utc)
        if expires_at < now:
            return EmailCodeVerifyResult(ok=False, error="expired_code")

        max_attempts = max(1, int(self.settings.EMAIL_CODE_MAX_ATTEMPTS))
        if int(latest_code.attempts or 0) >= max_attempts:
            return EmailCodeVerifyResult(ok=False, error="too_many_attempts")

        if len(normalized_code) != 6:
            latest_code.attempts = int(latest_code.attempts or 0) + 1
            throttle_result = await security_dal.record_throttle_failure(
                session,
                scope=security_dal.EMAIL_CODE_VERIFY_SCOPE,
                identifier=throttle_identifier,
                max_failures=self.settings.BRUTE_FORCE_MAX_FAILURES,
                window_seconds=self.settings.BRUTE_FORCE_WINDOW_SECONDS,
                lock_seconds=self.settings.BRUTE_FORCE_LOCK_SECONDS,
                now=now,
            )
            await session.flush()
            if throttle_result.locked:
                return EmailCodeVerifyResult(
                    ok=False,
                    error="rate_limited",
                    retry_after=throttle_result.retry_after,
                )
            if int(latest_code.attempts or 0) >= max_attempts:
                return EmailCodeVerifyResult(ok=False, error="too_many_attempts")
            return EmailCodeVerifyResult(ok=False, error="invalid_code")

        expected_hash = self._hash_code(normalized_email, purpose, normalized_code)
        if not hmac.compare_digest(expected_hash, latest_code.code_hash):
            latest_code.attempts = int(latest_code.attempts or 0) + 1
            throttle_result = await security_dal.record_throttle_failure(
                session,
                scope=security_dal.EMAIL_CODE_VERIFY_SCOPE,
                identifier=throttle_identifier,
                max_failures=self.settings.BRUTE_FORCE_MAX_FAILURES,
                window_seconds=self.settings.BRUTE_FORCE_WINDOW_SECONDS,
                lock_seconds=self.settings.BRUTE_FORCE_LOCK_SECONDS,
                now=now,
            )
            await session.flush()
            if throttle_result.locked:
                return EmailCodeVerifyResult(
                    ok=False,
                    error="rate_limited",
                    retry_after=throttle_result.retry_after,
                )
            return EmailCodeVerifyResult(ok=False, error="invalid_code")

        latest_code.consumed_at = now
        await security_dal.clear_throttle_state(
            session,
            scope=security_dal.EMAIL_CODE_VERIFY_SCOPE,
            identifier=throttle_identifier,
        )
        await session.flush()
        return EmailCodeVerifyResult(ok=True)

    async def _get_latest_code(
        self,
        session: AsyncSession,
        *,
        email: str,
        purpose: str,
        target_user_id: Optional[int],
    ) -> Optional[EmailVerificationCode]:
        stmt = (
            select(EmailVerificationCode)
            .where(
                EmailVerificationCode.email == email,
                EmailVerificationCode.purpose == purpose,
                EmailVerificationCode.target_user_id == target_user_id,
                EmailVerificationCode.status == "active",
                EmailVerificationCode.consumed_at.is_(None),
            )
            .order_by(EmailVerificationCode.created_at.desc())
            .limit(1)
        )
        result = await session.execute(stmt)
        return result.scalar_one_or_none()

    async def verify_magic_token(
        self,
        session: AsyncSession,
        *,
        token: str,
        purpose: str,
        target_user_id: Optional[int] = None,
    ) -> EmailMagicVerifyResult:
        if not token:
            return EmailMagicVerifyResult(ok=False, error="invalid_token")

        token_hash = self._hash_magic_token(token)
        now = datetime.now(timezone.utc)
        stmt = (
            select(EmailVerificationCode)
            .where(
                EmailVerificationCode.magic_token_hash == token_hash,
                EmailVerificationCode.purpose == purpose,
                EmailVerificationCode.target_user_id == target_user_id,
                EmailVerificationCode.status == "active",
                EmailVerificationCode.consumed_at.is_(None),
            )
            .limit(1)
        )
        result = await session.execute(stmt)
        record = result.scalar_one_or_none()
        if not record:
            return EmailMagicVerifyResult(ok=False, error="invalid_token")

        expires_at = record.expires_at
        if expires_at.tzinfo is None:
            expires_at = expires_at.replace(tzinfo=timezone.utc)
        if expires_at < now:
            return EmailMagicVerifyResult(ok=False, error="expired_token")

        record.consumed_at = now
        throttle_identifier = _email_throttle_identifier(
            record.email,
            purpose,
            target_user_id,
        )
        await security_dal.clear_throttle_state(
            session,
            scope=security_dal.EMAIL_CODE_VERIFY_SCOPE,
            identifier=throttle_identifier,
        )
        await session.flush()
        return EmailMagicVerifyResult(
            ok=True,
            email=record.email,
            purpose=record.purpose,
            target_user_id=record.target_user_id,
        )

    async def _send_code_email(
        self,
        *,
        email: str,
        code: str,
        language_code: str,
        magic_link: Optional[str] = None,
        purpose: str = "login",
    ) -> None:
        await asyncio.to_thread(
            self._send_code_email_sync,
            email=email,
            code=code,
            language_code=language_code,
            magic_link=magic_link,
            purpose=purpose,
        )

    async def send_custom_email(
        self,
        *,
        email: str,
        subject: str,
        body: str,
        html_body: Optional[str] = None,
    ) -> None:
        await asyncio.to_thread(
            self._send_custom_email_sync,
            email=email,
            subject=subject,
            body=body,
            html_body=html_body,
        )

    async def send_rendered_email(
        self,
        *,
        email: str,
        content: EmailContent,
    ) -> None:
        await self.send_custom_email(
            email=email,
            subject=content.subject,
            body=content.text,
            html_body=content.html,
        )

    def _send_code_email_sync(
        self,
        *,
        email: str,
        code: str,
        language_code: str,
        magic_link: Optional[str] = None,
        purpose: str = "login",
    ) -> None:
        content = render_login_code(
            self.settings,
            code=code,
            language_code=language_code,
            magic_link=magic_link,
            purpose=purpose,
            i18n=self.i18n,
        )

        message = EmailMessage()
        message["Subject"] = content.subject
        message["From"] = formataddr(
            (
                self.settings.SMTP_FROM_NAME or self.settings.WEBAPP_TITLE,
                self.settings.SMTP_FROM_EMAIL or "",
            )
        )
        message["To"] = email
        message.set_content(content.text)
        message.add_alternative(content.html, subtype="html")

        context = ssl.create_default_context()
        smtp_host = self.settings.SMTP_HOST
        timeout = max(5, int(self.settings.SMTP_TIMEOUT_SECONDS))
        attempts = self._smtp_attempts()
        last_error: Optional[BaseException] = None

        for attempt_number, attempt in enumerate(attempts, start=1):
            try:
                self._send_message_via_smtp(
                    message=message,
                    smtp_host=smtp_host,
                    smtp_port=attempt.port,
                    timeout=timeout,
                    context=context,
                    use_ssl=attempt.use_ssl,
                    starttls=attempt.starttls,
                )
                logger.info(
                    "Email verification code sent to %s via %s:%s",
                    email,
                    smtp_host,
                    attempt.port,
                )
                return
            except (OSError, smtplib.SMTPException, TimeoutError) as exc:
                last_error = exc
                log_level = logging.WARNING if attempt_number < len(attempts) else logging.ERROR
                logger.log(
                    log_level,
                    "SMTP send attempt %s/%s failed via %s:%s (ssl=%s, starttls=%s): %s",
                    attempt_number,
                    len(attempts),
                    smtp_host,
                    attempt.port,
                    attempt.use_ssl,
                    attempt.starttls,
                    exc,
                )

        if last_error:
            raise last_error

    def _send_custom_email_sync(
        self,
        *,
        email: str,
        subject: str,
        body: str,
        html_body: Optional[str] = None,
    ) -> None:
        message = EmailMessage()
        message["Subject"] = subject
        message["From"] = formataddr(
            (
                self.settings.SMTP_FROM_NAME or self.settings.WEBAPP_TITLE,
                self.settings.SMTP_FROM_EMAIL or "",
            )
        )
        message["To"] = email
        message.set_content(body)
        if html_body:
            message.add_alternative(html_body, subtype="html")

        context = ssl.create_default_context()
        smtp_host = self.settings.SMTP_HOST
        timeout = max(5, int(self.settings.SMTP_TIMEOUT_SECONDS))
        attempts = self._smtp_attempts()
        last_error: Optional[BaseException] = None

        for attempt_number, attempt in enumerate(attempts, start=1):
            try:
                self._send_message_via_smtp(
                    message=message,
                    smtp_host=smtp_host,
                    smtp_port=attempt.port,
                    timeout=timeout,
                    context=context,
                    use_ssl=attempt.use_ssl,
                    starttls=attempt.starttls,
                )
                logger.info(
                    "Custom email sent to %s via %s:%s",
                    email,
                    smtp_host,
                    attempt.port,
                )
                return
            except (OSError, smtplib.SMTPException, TimeoutError) as exc:
                last_error = exc
                log_level = logging.WARNING if attempt_number < len(attempts) else logging.ERROR
                logger.log(
                    log_level,
                    "SMTP send attempt %s/%s failed for custom email via %s:%s (ssl=%s, starttls=%s): %s",  # noqa: E501
                    attempt_number,
                    len(attempts),
                    smtp_host,
                    attempt.port,
                    attempt.use_ssl,
                    attempt.starttls,
                    exc,
                )

        if last_error:
            raise last_error

    def _send_message_via_smtp(
        self,
        *,
        message: EmailMessage,
        smtp_host: str,
        smtp_port: int,
        timeout: int,
        context: ssl.SSLContext,
        use_ssl: bool,
        starttls: bool,
    ) -> None:
        if use_ssl:
            with smtplib.SMTP_SSL(
                smtp_host,
                smtp_port,
                context=context,
                timeout=timeout,
            ) as smtp:
                smtp.ehlo()
                smtp.login(self.settings.SMTP_USERNAME, self.settings.SMTP_PASSWORD)
                smtp.send_message(message)
            return

        with smtplib.SMTP(smtp_host, smtp_port, timeout=timeout) as smtp:
            smtp.ehlo()
            if starttls:
                smtp.starttls(context=context)
                smtp.ehlo()
            smtp.login(self.settings.SMTP_USERNAME, self.settings.SMTP_PASSWORD)
            smtp.send_message(message)
