import json
import logging
import hmac
import re
import secrets
import time
from dataclasses import dataclass
from email.utils import formataddr
from hashlib import sha256
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode, urljoin
from urllib.request import Request, urlopen

from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection
from django.utils.html import strip_tags
from django.utils.html import escape
from wagtail.rich_text import expand_db_html

from .models import MailSmtpSettings, NewsletterSystemSettings, SystemNotificationMailSettings
from .security import decrypt_text

logger = logging.getLogger(__name__)


PLACEHOLDER_KEYS = (
    "token",
    "email",
    "list_id",
    "tenant_id",
    "confirm_url",
    "unsubscribe_url",
)


@dataclass
class APIResult:
    ok: bool
    status: int
    data: dict
    error: str = ""


def _decrypt_config_secret(value: str) -> str:
    encrypted_secret = (value or "").strip()
    if not encrypted_secret:
        return ""
    try:
        return decrypt_text(encrypted_secret)
    except Exception:
        return encrypted_secret


class MemberCenterClient:
    def __init__(self, config: NewsletterSystemSettings):
        self.config = config
        self._oauth_token_cache: dict[str, tuple[str, int]] = {}

    def subscribe(self, payload: dict) -> APIResult:
        return self._post(self.config.member_center_subscribe_path, payload, use_oauth=True)

    def confirm(self, token: str) -> APIResult:
        return self._get(self.config.member_center_confirm_path, {"token": token})

    def request_unsubscribe_token(self, payload: dict) -> APIResult:
        return self._post(self.config.member_center_unsubscribe_token_path, payload, use_oauth=True)

    def unsubscribe(self, payload: dict) -> APIResult:
        return self._post(self.config.member_center_unsubscribe_path, payload)

    def one_click_unsubscribe(self, payload: dict) -> APIResult:
        return self._post(self.config.member_center_one_click_unsubscribe_path, payload, use_oauth=True)

    def list_subscriptions(self, list_id: str) -> APIResult:
        auth_headers, auth_error = self._auth_headers()
        if auth_error:
            return APIResult(ok=False, status=0, data={}, error=auth_error)
        return self._get(
            self.config.member_center_subscriptions_path,
            {"list_id": list_id},
            headers=auth_headers,
        )

    def _get(self, path: str, query: dict, headers: dict | None = None, use_oauth: bool = False) -> APIResult:
        base_url = (self.config.member_center_base_url or "").strip()
        if not base_url:
            return APIResult(ok=False, status=0, data={}, error="member_center_base_url is empty")

        merged_headers = dict(headers or {})
        if use_oauth:
            auth_headers, auth_error = self._optional_auth_headers()
            if auth_error:
                return APIResult(ok=False, status=0, data={}, error=auth_error)
            merged_headers.update(auth_headers)

        endpoint = urljoin(f"{base_url.rstrip('/')}/", path.lstrip("/"))
        if query:
            endpoint = f"{endpoint}?{urlencode(query)}"

        request = Request(
            endpoint,
            headers={"Accept": "application/json", **merged_headers},
            method="GET",
        )
        timeout = max(1, int(self.config.member_center_timeout_seconds or 10))
        return self._send(request, timeout=timeout)

    def _post(self, path: str, payload: dict, headers: dict | None = None, use_oauth: bool = False) -> APIResult:
        base_url = (self.config.member_center_base_url or "").strip()
        if not base_url:
            return APIResult(ok=False, status=0, data={}, error="member_center_base_url is empty")

        merged_headers = dict(headers or {})
        if use_oauth:
            auth_headers, auth_error = self._optional_auth_headers()
            if auth_error:
                return APIResult(ok=False, status=0, data={}, error=auth_error)
            merged_headers.update(auth_headers)

        endpoint = urljoin(f"{base_url.rstrip('/')}/", path.lstrip("/"))
        body = json.dumps(payload).encode("utf-8")
        request = Request(
            endpoint,
            data=body,
            headers={"Content-Type": "application/json", **merged_headers},
            method="POST",
        )
        timeout = max(1, int(self.config.member_center_timeout_seconds or 10))
        return self._send(request, timeout=timeout)

    def _oauth_credentials_configured(self) -> bool:
        return bool(
            (self.config.member_center_oauth_client_id or "").strip()
            and (self.config.member_center_oauth_client_secret or "").strip()
        )

    def _optional_auth_headers(self) -> tuple[dict, str]:
        if not self._oauth_credentials_configured():
            return {}, ""
        return self._auth_headers()

    def _auth_headers(self) -> tuple[dict, str]:
        token, error = self._get_oauth_access_token()
        if not token:
            return {}, error or "member center oauth token is empty"
        return {"Authorization": f"Bearer {token}"}, ""

    def _get_oauth_access_token(self) -> tuple[str, str]:
        cache_key = "member_center"
        now = int(time.time())
        cached = self._oauth_token_cache.get(cache_key)
        if cached and now < cached[1] - 30:
            return cached[0], ""

        base_url = (self.config.member_center_base_url or "").strip()
        if not base_url:
            return "", "member_center_base_url is empty (cannot resolve oauth token url)"

        token_path = (self.config.member_center_oauth_token_path or "/oauth/token").strip()
        token_url = urljoin(f"{base_url.rstrip('/')}/", token_path.lstrip("/"))
        client_id = (self.config.member_center_oauth_client_id or "").strip()
        client_secret = _decrypt_config_secret(self.config.member_center_oauth_client_secret)
        if not client_id or not client_secret:
            return "", "member center oauth client_id/client_secret is empty"

        body = urlencode(
            {
                "grant_type": "client_credentials",
                "client_id": client_id,
                "client_secret": client_secret,
            }
        ).encode("utf-8")
        request = Request(
            token_url,
            data=body,
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            method="POST",
        )
        timeout = max(1, int(self.config.member_center_timeout_seconds or 10))
        result = self._send(request, timeout=timeout)
        if not result.ok:
            return "", f"oauth token request failed: HTTP {result.status} {result.error}".strip()

        access_token = str(result.data.get("access_token") or "").strip()
        if not access_token:
            return "", "oauth token response missing access_token"
        expires_in = int(result.data.get("expires_in") or 300)
        self._oauth_token_cache[cache_key] = (access_token, int(time.time()) + max(60, expires_in))
        return access_token, ""

    def _send(self, request: Request, timeout: int) -> APIResult:
        try:
            with urlopen(request, timeout=timeout) as response:
                raw = response.read().decode("utf-8")
                data = json.loads(raw) if raw else {}
                status = getattr(response, "status", 200)
                return APIResult(ok=200 <= status < 300, status=status, data=data)
        except HTTPError as exc:
            raw = exc.read().decode("utf-8") if exc.fp else ""
            try:
                data = json.loads(raw) if raw else {}
            except json.JSONDecodeError:
                data = {}
            return APIResult(ok=False, status=exc.code, data=data, error=str(exc))
        except (URLError, TimeoutError, ValueError, json.JSONDecodeError) as exc:
            return APIResult(ok=False, status=0, data={}, error=str(exc))


def render_placeholders(template: str, values: dict) -> str:
    rendered = template or ""
    for key in PLACEHOLDER_KEYS:
        rendered = rendered.replace(f"{{{{{key}}}}}", str(values.get(key, "")))
    return rendered


def _convert_draftail_contentstate_to_html(value: str) -> str:
    raw = (value or "").strip()
    if not raw or not raw.startswith("{"):
        return value or ""
    try:
        payload = json.loads(raw)
    except json.JSONDecodeError:
        return value or ""
    if not isinstance(payload, dict) or "blocks" not in payload:
        return value or ""
    try:
        from wagtail.admin.rich_text.converters.contentstate import ContentstateConverter

        converter = ContentstateConverter(features=["h2", "h3", "bold", "italic", "link", "image", "ul", "ol"])
        return converter.to_database_format(raw)
    except Exception:
        return value or ""


def _absolutize_links(html: str, site_base_url: str) -> str:
    base = (site_base_url or "").strip().rstrip("/")
    if not base:
        return html

    def _replace(match):
        prefix = match.group("prefix")
        url = match.group("url")
        return f"{prefix}{urljoin(f'{base}/', url.lstrip('/'))}"

    pattern = re.compile(r"(?P<prefix>\b(?:src|href)=['\"])(?P<url>/[^'\"]*)")
    return pattern.sub(_replace, html)


def _strip_inline_svg_placeholder_images(html: str) -> str:
    # Inline SVG data URI placeholders should not be sent as real email images.
    return re.sub(
        r"<img\b[^>]*\bsrc=['\"]data:image/svg\+xml;base64,[^'\"]*['\"][^>]*>",
        "",
        html or "",
        flags=re.IGNORECASE,
    )


def render_newsletter_html(template: str, values: dict, site_base_url: str = "") -> str:
    rendered = render_placeholders(template, values)
    try:
        rendered = expand_db_html(rendered)
    except Exception:
        pass
    return _absolutize_links(rendered, site_base_url)


def render_newsletter_html_for_send_job(template: str, site_base_url: str = "") -> str:
    rendered = _convert_draftail_contentstate_to_html(template or "")
    try:
        rendered = expand_db_html(rendered)
    except Exception:
        pass
    rendered = _strip_inline_svg_placeholder_images(rendered)
    return _absolutize_links(rendered, site_base_url)


def render_newsletter_text_for_send_job(template: str, site_base_url: str = "") -> str:
    html = render_newsletter_html_for_send_job(template or "", site_base_url=site_base_url)
    return strip_tags(html).strip()


def compose_newsletter_template_html(*, layout_html: str, email_body_html: str) -> str:
    layout = (layout_html or "").strip()
    body = _convert_draftail_contentstate_to_html(email_body_html or "")
    if not layout:
        return body
    if "{{email_body}}" in layout or "{{html_body}}" in layout or "{{text_body}}" in layout:
        return (
            layout.replace("{{email_body}}", body)
            .replace("{{html_body}}", body)
            .replace("{{text_body}}", body)
        )
    # If no placeholder is present, append body to avoid accidental body drop.
    return f"{layout}\n{body}" if body else layout


def compose_newsletter_template_text(*, layout_text: str, email_body_text: str) -> str:
    layout = (layout_text or "").strip()
    body = email_body_text or ""
    if not layout:
        return body
    if "{{email_body}}" in layout or "{{html_body}}" in layout or "{{text_body}}" in layout:
        return (
            layout.replace("{{email_body}}", body)
            .replace("{{html_body}}", body)
            .replace("{{text_body}}", body)
        )
    return f"{layout}\n{body}" if body else layout


def build_weekly_news_body_html(*, article_page_ids: list[int], item_template_html: str, site_base_url: str = "") -> str:
    ids = [int(v) for v in (article_page_ids or []) if str(v).isdigit()]
    if not ids:
        return ""

    from home.models import ArticlePage

    pages_by_id = {p.id: p for p in ArticlePage.objects.live().filter(id__in=ids)}
    blocks = []
    template = (item_template_html or "").strip()
    for pid in ids:
        page = pages_by_id.get(pid)
        if not page:
            continue
        cover_url = ""
        if page.cover_image_id and page.cover_image:
            try:
                cover_url = page.cover_image.get_rendition("fill-596x333").url
            except Exception:
                cover_url = page.cover_image.file.url
        if site_base_url and cover_url.startswith("/"):
            cover_url = urljoin(f"{site_base_url.rstrip('/')}/", cover_url.lstrip("/"))
        article_url = page.url or ""
        if site_base_url and article_url.startswith("/"):
            article_url = urljoin(f"{site_base_url.rstrip('/')}/", article_url.lstrip("/"))
        values = {
            "{{article_title}}": escape(page.title or ""),
            "{{article_url}}": escape(article_url),
            "{{article_cover_url}}": escape(cover_url),
            "{{article_intro}}": escape(page.intro or ""),
            "{{article_date}}": escape((page.date.strftime("%Y-%m-%d") if page.date else "")),
        }
        block = template
        for key, value in values.items():
            block = block.replace(key, value)
        blocks.append(block)
    return "\n".join(blocks)


def extract_token(payload: dict) -> str:
    if not payload:
        return ""

    for key in ("token", "confirm_token", "unsubscribe_token"):
        value = payload.get(key)
        if isinstance(value, str) and value.strip():
            return value.strip()

    data = payload.get("data")
    if isinstance(data, dict):
        for key in ("token", "confirm_token", "unsubscribe_token"):
            value = data.get(key)
            if isinstance(value, str) and value.strip():
                return value.strip()

    return ""


def build_from_email(sender_name: str, sender_email: str) -> str:
    if sender_name and sender_email:
        return formataddr((sender_name, sender_email))
    return sender_email


def resolve_one_click_secret(config: NewsletterSystemSettings) -> str:
    return (config.one_click_token_secret or "").strip() or settings.SECRET_KEY


def _b64url_encode(data: bytes) -> str:
    import base64

    return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")


def _b64url_decode(data: str) -> bytes:
    import base64

    padding = "=" * ((4 - len(data) % 4) % 4)
    return base64.urlsafe_b64decode((data + padding).encode("ascii"))


def generate_one_click_token(*, subscriber_id: str, list_id: str, site_id: str, campaign_id: str, secret: str, ttl_seconds: int) -> str:
    payload = {
        "subscriber_id": subscriber_id,
        "list_id": list_id,
        "site_id": site_id,
        "campaign_id": campaign_id,
        "exp": int(time.time()) + max(1, int(ttl_seconds)),
        "nonce": secrets.token_urlsafe(8),
    }
    payload_json = json.dumps(payload, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
    payload_part = _b64url_encode(payload_json)
    sig = hmac.new(secret.encode("utf-8"), payload_part.encode("ascii"), sha256).digest()
    sig_part = _b64url_encode(sig)
    return f"{payload_part}.{sig_part}"


def verify_one_click_token(token: str, secret: str) -> tuple[dict | None, str | None]:
    if not token or "." not in token:
        return None, "invalid"
    payload_part, sig_part = token.split(".", 1)
    try:
        received_sig = _b64url_decode(sig_part)
    except Exception:
        return None, "invalid"

    expected_sig = hmac.new(secret.encode("utf-8"), payload_part.encode("ascii"), sha256).digest()
    if not hmac.compare_digest(received_sig, expected_sig):
        return None, "invalid"

    try:
        payload = json.loads(_b64url_decode(payload_part).decode("utf-8"))
    except Exception:
        return None, "invalid"

    required_keys = ("subscriber_id", "list_id", "site_id", "exp")
    if any(not payload.get(key) for key in required_keys):
        return None, "invalid"
    if int(payload.get("exp", 0)) < int(time.time()):
        return None, "expired"
    return payload, None


def build_one_click_unsubscribe_url(*, site_base_url: str, endpoint_path: str, token: str) -> str:
    base = site_base_url.rstrip("/")
    endpoint = endpoint_path if endpoint_path.startswith("/") else f"/{endpoint_path}"
    return f"{base}{endpoint}?{urlencode({'token': token})}"


def build_list_unsubscribe_headers(*, one_click_url: str) -> dict:
    return {
        "List-Unsubscribe": f"<{one_click_url}>",
        "List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
    }


class SendEngineClient:
    def __init__(self, config: NewsletterSystemSettings):
        self.config = config
        self._oauth_token_cache: dict[str, tuple[str, int]] = {}

    def create_send_job(self, payload: dict) -> APIResult:
        base_url = (self.config.send_engine_base_url or "").strip()
        if not base_url:
            return APIResult(ok=False, status=0, data={}, error="send_engine_base_url is empty")

        endpoint = urljoin(f"{base_url.rstrip('/')}/", self.config.send_engine_send_jobs_path.lstrip("/"))
        body = json.dumps(payload).encode("utf-8")
        auth_headers, auth_error = self._auth_headers(required_scope="newsletter:send.write")
        if auth_error:
            return APIResult(ok=False, status=0, data={}, error=auth_error)

        headers = {"Content-Type": "application/json", **auth_headers}

        request = Request(endpoint, data=body, headers=headers, method="POST")
        timeout = max(1, int(self.config.send_engine_timeout_seconds or 10))
        return self._send(request, timeout=timeout)

    def get_send_job(self, send_job_id: str) -> APIResult:
        base_url = (self.config.send_engine_base_url or "").strip()
        if not base_url:
            return APIResult(ok=False, status=0, data={}, error="send_engine_base_url is empty")
        if not send_job_id:
            return APIResult(ok=False, status=0, data={}, error="send_job_id is empty")

        endpoint = urljoin(
            f"{base_url.rstrip('/')}/",
            f"{self.config.send_engine_send_jobs_path.lstrip('/')}/{send_job_id}",
        )
        auth_headers, auth_error = self._auth_headers(required_scope="newsletter:send.read")
        if auth_error:
            return APIResult(ok=False, status=0, data={}, error=auth_error)

        headers = {"Accept": "application/json", **auth_headers}

        request = Request(endpoint, headers=headers, method="GET")
        timeout = max(1, int(self.config.send_engine_timeout_seconds or 10))
        return self._send(request, timeout=timeout)

    def _auth_headers(self, required_scope: str = "") -> tuple[dict, str]:
        token, error = self._get_member_center_access_token(required_scope=required_scope)
        if error:
            return {}, error
        return {"Authorization": f"Bearer {token}"}, ""

    def _get_member_center_access_token(self, required_scope: str = "") -> tuple[str, str]:
        client_id = (
            (getattr(self.config, "send_engine_oauth_client_id", "") or "").strip()
            or (self.config.member_center_oauth_client_id or "").strip()
        )
        encrypted_secret = (
            (getattr(self.config, "send_engine_oauth_client_secret", "") or "").strip()
            or (self.config.member_center_oauth_client_secret or "").strip()
        )
        client_secret = _decrypt_config_secret(encrypted_secret)
        cache_key = client_id or "send_engine"
        now = int(time.time())
        cached = self._oauth_token_cache.get(cache_key)
        if cached and now < cached[1] - 30:
            return cached[0], ""

        member_center_base_url = (self.config.member_center_base_url or "").strip()
        if not member_center_base_url:
            return "", "member_center_base_url is empty (cannot resolve oauth token url)"
        token_path = (self.config.member_center_oauth_token_path or "/oauth/token").strip()
        token_url = urljoin(f"{member_center_base_url.rstrip('/')}/", token_path.lstrip("/"))

        if not client_id or not client_secret:
            return ("", "send engine oauth client_id/client_secret is empty")
        body = urlencode(
            {
                "grant_type": "client_credentials",
                "client_id": client_id,
                "client_secret": client_secret,
            }
        ).encode("utf-8")
        request = Request(
            token_url,
            data=body,
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            method="POST",
        )
        timeout = max(1, int(self.config.send_engine_timeout_seconds or 10))
        result = self._send(request, timeout=timeout)
        if not result.ok:
            return "", f"oauth token request failed: HTTP {result.status} {result.error}".strip()

        access_token = str(result.data.get("access_token") or "").strip()
        if not access_token:
            return "", "oauth token response missing access_token"

        expires_in = int(result.data.get("expires_in") or 300)
        self._oauth_token_cache[cache_key] = (access_token, int(time.time()) + max(60, expires_in))
        return access_token, ""

    def _send(self, request: Request, timeout: int) -> APIResult:
        try:
            with urlopen(request, timeout=timeout) as response:
                raw = response.read().decode("utf-8")
                data = json.loads(raw) if raw else {}
                status = getattr(response, "status", 200)
                return APIResult(ok=200 <= status < 300, status=status, data=data)
        except HTTPError as exc:
            raw = exc.read().decode("utf-8") if exc.fp else ""
            try:
                data = json.loads(raw) if raw else {}
            except json.JSONDecodeError:
                data = {}
            return APIResult(ok=False, status=exc.code, data=data, error=str(exc))
        except (URLError, TimeoutError, ValueError, json.JSONDecodeError) as exc:
            return APIResult(ok=False, status=0, data={}, error=str(exc))


def _build_smtp_connection(smtp_config: MailSmtpSettings):
    if not smtp_config.smtp_relay_host:
        raise ValueError("SMTP relay host is empty. Please save SMTP settings first.")
    if smtp_config.smtp_use_tls and smtp_config.smtp_use_ssl:
        raise ValueError("SMTP TLS and SSL cannot both be enabled.")

    password = ""
    encrypted_password = (smtp_config.smtp_password or "").strip()
    if encrypted_password:
        try:
            password = decrypt_text(encrypted_password)
        except Exception:
            password = ""

    return get_connection(
        backend="django.core.mail.backends.smtp.EmailBackend",
        host=smtp_config.smtp_relay_host,
        port=smtp_config.smtp_relay_port,
        username=smtp_config.smtp_username or None,
        password=password or None,
        use_tls=bool(smtp_config.smtp_use_tls),
        use_ssl=bool(smtp_config.smtp_use_ssl),
        timeout=max(1, int(smtp_config.smtp_timeout_seconds or 15)),
        fail_silently=False,
    )


def send_subscribe_email(
    *,
    to_email: str,
    subject: str,
    text_body: str,
    html_body: str,
    config: NewsletterSystemSettings,
    smtp_config: MailSmtpSettings | None = None,
) -> int:
    smtp_config = smtp_config or MailSmtpSettings.load()
    from_email = build_from_email(config.sender_name, config.sender_email)
    reply_to = [config.reply_to_email] if config.reply_to_email else None
    connection = _build_smtp_connection(smtp_config)

    message = EmailMultiAlternatives(
        subject=subject,
        body=text_body,
        from_email=from_email or settings.DEFAULT_FROM_EMAIL,
        to=[to_email],
        reply_to=reply_to,
        connection=connection,
    )
    if html_body:
        message.attach_alternative(html_body, "text/html")

    charset = (config.default_charset or "utf-8").strip() or "utf-8"
    message.encoding = charset
    sent_count = message.send(fail_silently=False)
    logger.info(
        "newsletter email send result sent_count=%s smtp_host=%s smtp_port=%s smtp_tls=%s smtp_ssl=%s smtp_timeout=%s from_email=%s to_email=%s",
        sent_count,
        smtp_config.smtp_relay_host,
        smtp_config.smtp_relay_port,
        bool(smtp_config.smtp_use_tls),
        bool(smtp_config.smtp_use_ssl),
        max(1, int(smtp_config.smtp_timeout_seconds or 15)),
        from_email or settings.DEFAULT_FROM_EMAIL,
        to_email,
    )
    return sent_count


def send_contact_notification_email(
    *,
    subject: str,
    text_body: str,
    html_body: str,
    notification_config: SystemNotificationMailSettings,
    smtp_config: MailSmtpSettings | None = None,
) -> int:
    recipients = notification_config.contact_form_recipient_list()
    if not recipients:
        return 0

    smtp_config = smtp_config or MailSmtpSettings.load()
    from_email = build_from_email(
        notification_config.contact_form_from_name,
        notification_config.contact_form_from_email,
    )
    reply_to = (
        [notification_config.contact_form_reply_to_email]
        if notification_config.contact_form_reply_to_email
        else None
    )
    connection = _build_smtp_connection(smtp_config)
    message = EmailMultiAlternatives(
        subject=subject,
        body=text_body,
        from_email=from_email or settings.DEFAULT_FROM_EMAIL,
        to=recipients,
        reply_to=reply_to,
        connection=connection,
    )
    if html_body:
        message.attach_alternative(html_body, "text/html")
    charset = (notification_config.default_charset or "utf-8").strip() or "utf-8"
    message.encoding = charset
    return message.send(fail_silently=False)


def send_contact_user_email(
    *,
    to_email: str,
    subject: str,
    text_body: str,
    html_body: str,
    notification_config: SystemNotificationMailSettings,
    smtp_config: MailSmtpSettings | None = None,
) -> int:
    if not (to_email or "").strip():
        return 0

    smtp_config = smtp_config or MailSmtpSettings.load()
    from_email = build_from_email(
        notification_config.contact_form_from_name,
        notification_config.contact_form_from_email,
    )
    reply_to = (
        [notification_config.contact_form_reply_to_email]
        if notification_config.contact_form_reply_to_email
        else None
    )
    connection = _build_smtp_connection(smtp_config)
    message = EmailMultiAlternatives(
        subject=subject,
        body=text_body,
        from_email=from_email or settings.DEFAULT_FROM_EMAIL,
        to=[to_email.strip()],
        reply_to=reply_to,
        connection=connection,
    )
    if html_body:
        message.attach_alternative(html_body, "text/html")
    charset = (notification_config.default_charset or "utf-8").strip() or "utf-8"
    message.encoding = charset
    return message.send(fail_silently=False)
