# retoor <retoor@molodetz.nl>

from __future__ import annotations

import asyncio
import json
import logging
from typing import Any

from devplacepy import database
from devplacepy.services.email import EmailAccount, EmailClient, EmailError
from devplacepy.services.email.client import imap_quote

from ..config import Settings
from ..errors import AuthRequiredError, ToolInputError, UpstreamError

logger = logging.getLogger("devii.email")

ACCOUNT_FIELDS = (
    "imap_host",
    "imap_port",
    "imap_ssl",
    "imap_starttls",
    "smtp_host",
    "smtp_port",
    "smtp_ssl",
    "smtp_starttls",
    "username",
    "password",
    "from_address",
    "from_name",
)
BOOLEAN_FIELDS = ("imap_ssl", "imap_starttls", "smtp_ssl", "smtp_starttls")
INTEGER_FIELDS = ("imap_port", "smtp_port")
MARK_FLAGS = {
    "read": ("\\Seen", True),
    "unread": ("\\Seen", False),
    "flagged": ("\\Flagged", True),
    "unflagged": ("\\Flagged", False),
}


def _flag(value: Any) -> bool:
    if isinstance(value, bool):
        return value
    return str(value).strip().lower() in ("1", "true", "yes", "on")


def _text(arguments: dict[str, Any], key: str) -> str:
    return str(arguments.get(key, "") or "").strip()


def _address_list(value: Any) -> list[str]:
    if isinstance(value, (list, tuple)):
        items = [str(item).strip() for item in value]
    else:
        items = [chunk.strip() for chunk in str(value or "").split(",")]
    return [item for item in items if item]


class EmailController:
    def __init__(
        self, settings: Settings, owner_kind: str = "guest", owner_id: str = ""
    ) -> None:
        self._settings = settings
        self._owner_kind = owner_kind
        self._owner_id = owner_id

    async def dispatch(self, name: str, arguments: dict[str, Any]) -> str:
        if not getattr(self._settings, "email_enabled", True):
            raise ToolInputError(
                "Email tools are disabled by the administrator."
            )
        if self._owner_kind != "user":
            raise AuthRequiredError(
                "Email tools are only available to signed-in users.", tool=name
            )
        if name == "email_accounts_list":
            return self._accounts_list()
        if name == "email_account_get":
            return self._account_get(arguments)
        if name == "email_account_set":
            return self._account_set(arguments)
        if name == "email_account_delete":
            return self._account_delete(arguments)
        if name == "email_list_folders":
            return await self._list_folders(arguments)
        if name == "email_list_messages":
            return await self._list_messages(arguments)
        if name == "email_read_message":
            return await self._read_message(arguments)
        if name == "email_search":
            return await self._search(arguments)
        if name == "email_set_flags":
            return await self._set_flags(arguments)
        if name == "email_mark":
            return await self._mark(arguments)
        if name == "email_move_message":
            return await self._move(arguments)
        if name == "email_delete_message":
            return await self._delete(arguments)
        if name == "email_send":
            return await self._send(arguments)
        raise ToolInputError(f"Unknown email tool: {name}")

    def _label(self, arguments: dict[str, Any]) -> str:
        label = _text(arguments, "account")
        if not label:
            raise ToolInputError("An 'account' label is required.")
        return label

    def _client(self, arguments: dict[str, Any]) -> EmailClient:
        label = self._label(arguments)
        row = database.get_email_account(self._owner_kind, self._owner_id, label)
        if not row:
            raise ToolInputError(
                f"No email account named '{label}'. Add it with email_account_set first."
            )
        return EmailClient(
            EmailAccount.from_row(row),
            timeout=getattr(self._settings, "email_timeout_seconds", 30.0),
        )

    def _public_account(self, row: dict[str, Any]) -> dict[str, Any]:
        return {
            "label": row.get("label"),
            "imap_host": row.get("imap_host"),
            "imap_port": row.get("imap_port"),
            "imap_ssl": bool(row.get("imap_ssl")),
            "imap_starttls": bool(row.get("imap_starttls")),
            "smtp_host": row.get("smtp_host"),
            "smtp_port": row.get("smtp_port"),
            "smtp_ssl": bool(row.get("smtp_ssl")),
            "smtp_starttls": bool(row.get("smtp_starttls")),
            "username": row.get("username"),
            "password_set": bool(row.get("password")),
            "from_address": row.get("from_address"),
            "from_name": row.get("from_name"),
            "updated_at": row.get("updated_at"),
        }

    def _accounts_list(self) -> str:
        rows = database.list_email_accounts(self._owner_kind, self._owner_id)
        return json.dumps(
            {
                "status": "success",
                "accounts": [self._public_account(row) for row in rows],
            },
            ensure_ascii=False,
        )

    def _account_get(self, arguments: dict[str, Any]) -> str:
        label = self._label(arguments)
        row = database.get_email_account(self._owner_kind, self._owner_id, label)
        if not row:
            raise ToolInputError(f"No email account named '{label}'.")
        return json.dumps(
            {"status": "success", "account": self._public_account(row)},
            ensure_ascii=False,
        )

    def _account_set(self, arguments: dict[str, Any]) -> str:
        label = self._label(arguments)
        fields: dict[str, Any] = {}
        for key in ACCOUNT_FIELDS:
            if key not in arguments or arguments[key] is None:
                continue
            value = arguments[key]
            if key in BOOLEAN_FIELDS:
                fields[key] = 1 if _flag(value) else 0
            elif key in INTEGER_FIELDS:
                try:
                    fields[key] = int(value)
                except (TypeError, ValueError):
                    raise ToolInputError(f"'{key}' must be an integer port number.")
            else:
                fields[key] = str(value).strip()
        row = database.set_email_account(
            self._owner_kind, self._owner_id, label, fields
        )
        return json.dumps(
            {
                "status": "success",
                "saved": True,
                "account": self._public_account(row),
            },
            ensure_ascii=False,
        )

    def _account_delete(self, arguments: dict[str, Any]) -> str:
        label = self._label(arguments)
        removed = database.delete_email_account(
            self._owner_kind,
            self._owner_id,
            label,
            deleted_by=f"user:{self._owner_id}",
        )
        return json.dumps(
            {"status": "success", "removed": int(removed), "account": label},
            ensure_ascii=False,
        )

    def _folder(self, arguments: dict[str, Any]) -> str:
        return _text(arguments, "folder") or "INBOX"

    def _limit(self, arguments: dict[str, Any], default: int = 25) -> int:
        try:
            limit = int(arguments.get("limit", default))
        except (TypeError, ValueError):
            limit = default
        return max(1, min(limit, 100))

    def _offset(self, arguments: dict[str, Any]) -> int:
        try:
            offset = int(arguments.get("offset", 0))
        except (TypeError, ValueError):
            offset = 0
        return max(0, offset)

    def _criteria(self, arguments: dict[str, Any]) -> list[str]:
        criteria: list[str] = []
        if _flag(arguments.get("unseen")):
            criteria.append("UNSEEN")
        if _flag(arguments.get("seen")):
            criteria.append("SEEN")
        if _flag(arguments.get("flagged")):
            criteria.append("FLAGGED")
        sender = _text(arguments, "from")
        if sender:
            criteria += ["FROM", imap_quote(sender)]
        subject = _text(arguments, "subject")
        if subject:
            criteria += ["SUBJECT", imap_quote(subject)]
        since = _text(arguments, "since")
        if since:
            criteria += ["SINCE", since]
        text = _text(arguments, "text")
        if text:
            criteria += ["TEXT", imap_quote(text)]
        return criteria

    async def _run(self, func: Any, *args: Any) -> Any:
        try:
            return await asyncio.to_thread(func, *args)
        except EmailError as exc:
            if exc.kind in ("config", "not_found", "blocked"):
                raise ToolInputError(exc.message) from exc
            raise UpstreamError(exc.message) from exc

    def _ok(self, payload: dict[str, Any]) -> str:
        return json.dumps({"status": "success", **payload}, ensure_ascii=False)

    async def _list_folders(self, arguments: dict[str, Any]) -> str:
        client = self._client(arguments)
        folders = await self._run(client.list_folders)
        return self._ok({"folders": folders})

    async def _list_messages(self, arguments: dict[str, Any]) -> str:
        client = self._client(arguments)
        result = await self._run(
            client.list_messages,
            self._folder(arguments),
            self._criteria(arguments),
            self._limit(arguments),
            self._offset(arguments),
        )
        return self._ok(result)

    async def _read_message(self, arguments: dict[str, Any]) -> str:
        client = self._client(arguments)
        uid = _text(arguments, "uid")
        if not uid:
            raise ToolInputError("A message 'uid' is required.")
        result = await self._run(
            client.read_message, self._folder(arguments), uid
        )
        return self._ok({"message": result})

    async def _search(self, arguments: dict[str, Any]) -> str:
        client = self._client(arguments)
        result = await self._run(
            client.search,
            self._folder(arguments),
            self._criteria(arguments),
            self._limit(arguments),
        )
        return self._ok(result)

    async def _set_flags(self, arguments: dict[str, Any]) -> str:
        client = self._client(arguments)
        uid = _text(arguments, "uid")
        if not uid:
            raise ToolInputError("A message 'uid' is required.")
        flags = _address_list(arguments.get("flags"))
        if not flags:
            raise ToolInputError("At least one flag is required (e.g. \\Seen).")
        add = _flag(arguments.get("add", True)) if "add" in arguments else True
        result = await self._run(
            client.set_flags, self._folder(arguments), uid, flags, add
        )
        return self._ok(result)

    async def _mark(self, arguments: dict[str, Any]) -> str:
        client = self._client(arguments)
        uid = _text(arguments, "uid")
        if not uid:
            raise ToolInputError("A message 'uid' is required.")
        state = _text(arguments, "state").lower()
        if state not in MARK_FLAGS:
            raise ToolInputError(
                "'state' must be one of: read, unread, flagged, unflagged."
            )
        flag, add = MARK_FLAGS[state]
        result = await self._run(
            client.set_flags, self._folder(arguments), uid, [flag], add
        )
        return self._ok({**result, "state": state})

    async def _move(self, arguments: dict[str, Any]) -> str:
        client = self._client(arguments)
        uid = _text(arguments, "uid")
        destination = _text(arguments, "destination")
        if not uid or not destination:
            raise ToolInputError("Both 'uid' and 'destination' are required.")
        result = await self._run(
            client.move_message, self._folder(arguments), uid, destination
        )
        return self._ok(result)

    async def _delete(self, arguments: dict[str, Any]) -> str:
        client = self._client(arguments)
        uid = _text(arguments, "uid")
        if not uid:
            raise ToolInputError("A message 'uid' is required.")
        trash = _text(arguments, "trash") or None
        result = await self._run(
            client.delete_message, self._folder(arguments), uid, trash
        )
        return self._ok(result)

    async def _send(self, arguments: dict[str, Any]) -> str:
        client = self._client(arguments)
        to = _address_list(arguments.get("to"))
        if not to:
            raise ToolInputError("At least one 'to' recipient is required.")
        subject = _text(arguments, "subject")
        body = str(arguments.get("body", "") or "")
        if not subject and not body:
            raise ToolInputError("A 'subject' or 'body' is required.")
        result = await self._run(
            client.send_message,
            to,
            subject,
            body,
            _address_list(arguments.get("cc")),
            _address_list(arguments.get("bcc")),
            str(arguments.get("html", "") or "") or None,
            _text(arguments, "in_reply_to") or None,
        )
        return self._ok(result)
