from __future__ import annotations

import base64
import binascii
import imaplib
import re
import smtplib
import ssl
from collections.abc import Iterator
from contextlib import contextmanager, suppress
from email.parser import BytesParser
from email.policy import default as email_default_policy

from ccma.storage.repository import RepositoryError


def _ssl_context() -> ssl.SSLContext:
    return ssl.create_default_context()


def _smtp_client(settings: dict) -> smtplib.SMTP:
    host = str(settings.get("smtp_host", "")).strip()
    if not host:
        raise RepositoryError("Kein SMTP-Server konfiguriert.")
    port = int(settings.get("smtp_port", 587))
    security = str(settings.get("smtp_security", "starttls"))
    client = (
        smtplib.SMTP_SSL(host, port, context=_ssl_context(), timeout=30)
        if security == "ssl"
        else smtplib.SMTP(host, port, timeout=30)
    )
    if security == "starttls":
        client.starttls(context=_ssl_context())
    username = str(settings.get("smtp_username", "")).strip()
    if username:
        client.login(username, str(settings.get("smtp_password", "")))
    return client


def _imap_client(settings: dict) -> imaplib.IMAP4:
    host = str(settings.get("imap_host", "")).strip()
    if not host:
        raise RepositoryError("Kein IMAP-Server konfiguriert.")
    port = int(settings.get("imap_port", 993))
    security = str(settings.get("imap_security", "ssl"))
    client = (
        imaplib.IMAP4_SSL(host, port, timeout=30)
        if security == "ssl"
        else imaplib.IMAP4(host, port, timeout=30)
    )
    if security == "starttls":
        client.starttls(_ssl_context())
    username = str(settings.get("imap_username", "")).strip()
    if username:
        client.login(username, str(settings.get("imap_password", "")))
    return client


@contextmanager
def smtp_session(settings: dict) -> Iterator[smtplib.SMTP]:
    """Opens one authenticated SMTP connection to reuse across several sends (a SEPA
    info-mail batch may cover dozens of members -- reconnecting/re-authenticating per
    message would be slow and can trigger provider rate limits)."""
    try:
        client = _smtp_client(settings)
    except (OSError, smtplib.SMTPException) as exc:
        raise RepositoryError(f"SMTP-Verbindung fehlgeschlagen: {exc}") from exc
    try:
        yield client
    finally:
        with suppress(OSError, smtplib.SMTPException):
            client.quit()


@contextmanager
def imap_session(settings: dict) -> Iterator[imaplib.IMAP4]:
    try:
        client = _imap_client(settings)
    except (OSError, imaplib.IMAP4.error) as exc:
        raise RepositoryError(f"IMAP-Verbindung fehlgeschlagen: {exc}") from exc
    try:
        yield client
    finally:
        with suppress(OSError, imaplib.IMAP4.error):
            client.logout()


def send_via_smtp(client: smtplib.SMTP, message_bytes: bytes) -> None:
    message = BytesParser(policy=email_default_policy).parsebytes(message_bytes)
    try:
        client.send_message(message)
    except (OSError, smtplib.SMTPException) as exc:
        raise RepositoryError(f"E-Mail konnte nicht versandt werden: {exc}") from exc


def append_message(client: imaplib.IMAP4, message_bytes: bytes, *, folder: str, flags: str) -> None:
    try:
        status, response = client.append(_encode_imap_utf7(folder), flags, None, message_bytes)
    except (OSError, imaplib.IMAP4.error) as exc:
        raise RepositoryError(f"E-Mail konnte nicht in „{folder}“ abgelegt werden: {exc}") from exc
    if status != "OK":
        detail = response[0].decode("utf-8", "replace") if response and response[0] else status
        raise RepositoryError(f"IMAP-Server hat die Ablage im Ordner „{folder}“ abgelehnt: {detail}")


def ensure_imap_folder(client: imaplib.IMAP4, folder: str) -> None:
    """Creates the folder on the server if it doesn't exist yet -- lets someone type
    a not-yet-existing Entwürfe/Gesendet folder name and have it just appear, instead
    of every append failing until it's created by hand in a separate mail client."""
    encoded = _encode_imap_utf7(folder)
    try:
        status, _response = client.select(encoded, readonly=True)
    except (OSError, imaplib.IMAP4.error) as exc:
        raise RepositoryError(f"IMAP-Ordner „{folder}“ konnte nicht geprüft werden: {exc}") from exc
    if status == "OK":
        return
    try:
        status, response = client.create(encoded)
    except (OSError, imaplib.IMAP4.error) as exc:
        raise RepositoryError(f"IMAP-Ordner „{folder}“ konnte nicht angelegt werden: {exc}") from exc
    if status != "OK":
        detail = response[0].decode("utf-8", "replace") if response and response[0] else status
        raise RepositoryError(f"IMAP-Ordner „{folder}“ konnte nicht angelegt werden: {detail}")


_LIST_RESPONSE_RE = re.compile(
    rb'^\((?P<flags>[^)]*)\)\s+(?:"(?P<qdelim>[^"]*)"|(?P<delim>\S+))\s+(?P<name>.+)$'
)


def list_imap_folders(settings: dict) -> list[str]:
    """Human-readable (decoded) folder names for the Options dialog's folder picker
    -- fetched live from the server, so it only works once credentials are entered."""
    with imap_session(settings) as client:
        try:
            status, response = client.list()
        except (OSError, imaplib.IMAP4.error) as exc:
            raise RepositoryError(f"Ordnerliste konnte nicht abgerufen werden: {exc}") from exc
        if status != "OK":
            raise RepositoryError("Ordnerliste konnte nicht abgerufen werden.")
        folders = []
        for raw in response or []:
            if not raw:
                continue
            name = _parse_list_response(raw)
            if name:
                folders.append(name)
        return folders


def _parse_list_response(raw: bytes) -> str | None:
    match = _LIST_RESPONSE_RE.match(raw)
    if not match:
        return None
    name = match.group("name").decode("utf-8", "replace").strip()
    if name.startswith('"') and name.endswith('"') and len(name) >= 2:
        name = name[1:-1]
    return _decode_imap_utf7(name)


def _decode_imap_utf7(value: str) -> str:
    # Modified UTF-7 (RFC 3501 5.1.3): "&" takes the role of "+", and "," takes the
    # role of "/" inside the base64 run; "&-" is a literal ampersand.
    if "&" not in value:
        return value
    result: list[str] = []
    index = 0
    length = len(value)
    while index < length:
        char = value[index]
        if char != "&":
            result.append(char)
            index += 1
            continue
        end = value.find("-", index + 1)
        if end == -1:
            end = length
        chunk = value[index + 1 : end]
        if chunk == "":
            result.append("&")
        else:
            base64_chunk = chunk.replace(",", "/")
            base64_chunk += "=" * (-len(base64_chunk) % 4)
            try:
                result.append(base64.b64decode(base64_chunk).decode("utf-16-be"))
            except (binascii.Error, UnicodeDecodeError):
                result.append("&" + chunk + "-")
        index = end + 1
    return "".join(result)


def _encode_imap_utf7(value: str) -> str:
    if "&" not in value and all(32 <= ord(char) <= 126 for char in value):
        return value
    result: list[str] = []
    index = 0
    length = len(value)
    while index < length:
        char = value[index]
        if char == "&":
            result.append("&-")
            index += 1
            continue
        if 32 <= ord(char) <= 126:
            result.append(char)
            index += 1
            continue
        start = index
        while index < length and not (32 <= ord(value[index]) <= 126):
            index += 1
        chunk = value[start:index]
        encoded = base64.b64encode(chunk.encode("utf-16-be")).decode("ascii").rstrip("=")
        result.append("&" + encoded.replace("/", ",") + "-")
    return "".join(result)


def test_smtp_connection(settings: dict) -> None:
    with smtp_session(settings):
        pass


def test_imap_connection(settings: dict) -> None:
    with imap_session(settings):
        pass
