import json
import logging
import time
import uuid
from dataclasses import dataclass
from datetime import timedelta
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

from django.conf import settings
from django.db import transaction
from django.db.models import Q
from django.utils import timezone
from django.utils.html import escape

from .models import (
    ContactFormSettings,
    ContactFormSubmission,
    ContactSpamDetectionState,
    MailSmtpSettings,
    SystemNotificationMailSettings,
)
from .newsletter import send_contact_notification_email, send_contact_user_email

logger = logging.getLogger(__name__)


class SpamDetectionError(Exception):
    pass


@dataclass(frozen=True)
class SpamDetectionResult:
    category: int
    source: str


class SpamDetectionClient:
    def __init__(self):
        self.url = settings.CONTACT_SPAM_DETECTION_URL
        self.api_key = settings.CONTACT_SPAM_DETECTION_API_KEY
        self.timeout_seconds = max(1, int(settings.CONTACT_SPAM_DETECTION_TIMEOUT_SECONDS))

    def validate_config(self):
        if not self.url:
            raise SpamDetectionError("CONTACT_SPAM_DETECTION_URL is not configured")
        if not self.api_key:
            raise SpamDetectionError("CONTACT_SPAM_DETECTION_API_KEY is not configured")

    def classify(self, message: str) -> SpamDetectionResult:
        self.validate_config()
        request = Request(
            self.url,
            data=json.dumps({"message": message}).encode("utf-8"),
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
                "Accept": "application/json",
            },
            method="POST",
        )
        try:
            with urlopen(request, timeout=self.timeout_seconds) as response:
                response_data = json.loads(response.read().decode("utf-8"))
        except HTTPError as exc:
            try:
                detail = exc.read().decode("utf-8")
            except Exception:
                detail = ""
            raise SpamDetectionError(f"spam detection HTTP {exc.code}: {detail[:500]}") from exc
        except (URLError, TimeoutError, OSError) as exc:
            raise SpamDetectionError(f"spam detection request failed: {exc}") from exc
        except (UnicodeDecodeError, json.JSONDecodeError) as exc:
            raise SpamDetectionError("spam detection returned invalid JSON") from exc

        if not isinstance(response_data, dict):
            raise SpamDetectionError("spam detection response must be an object")
        if response_data.get("error"):
            raise SpamDetectionError(f"spam detection service error: {response_data['error']}")

        category = response_data.get("category")
        if type(category) is not int or category not in {0, 1, 2, 3}:
            raise SpamDetectionError(f"spam detection returned invalid category: {category!r}")
        return SpamDetectionResult(
            category=category,
            source=str(response_data.get("source") or "")[:32],
        )


def _render_contact_template(template: str, values: dict[str, str]) -> str:
    rendered = template or ""
    for key, value in values.items():
        rendered = rendered.replace(f"{{{{{key}}}}}", value)
    return rendered


def _claim_mail_submission(now):
    stale_before = now - timedelta(seconds=max(1, int(settings.CONTACT_FORM_WORKER_LEASE_SECONDS)))
    eligible = Q(
        processing_status=ContactFormSubmission.STATUS_SEND_RETRY,
        next_attempt_at__lte=now,
    ) | Q(
        processing_status=ContactFormSubmission.STATUS_SEND_RETRY,
        next_attempt_at__isnull=True,
    ) | Q(
        processing_status=ContactFormSubmission.STATUS_SENDING,
        processing_started_at__lte=stale_before,
    ) | Q(
        processing_status=ContactFormSubmission.STATUS_SENDING,
        processing_started_at__isnull=True,
    )
    with transaction.atomic():
        submission = (
            ContactFormSubmission.objects.select_for_update(skip_locked=True)
            .filter(eligible)
            .order_by("created_at", "id")
            .first()
        )
        if submission is None:
            return None
        submission.processing_status = ContactFormSubmission.STATUS_SENDING
        submission.processing_started_at = now
        submission.next_attempt_at = None
        submission.save(update_fields=["processing_status", "processing_started_at", "next_attempt_at", "updated_at"])
        return submission


def _claim_scan_submission(now):
    stale_before = now - timedelta(seconds=max(1, int(settings.CONTACT_FORM_WORKER_LEASE_SECONDS)))
    eligible = Q(processing_status=ContactFormSubmission.STATUS_QUEUED) | Q(
        processing_status=ContactFormSubmission.STATUS_SCAN_RETRY,
        next_attempt_at__lte=now,
    ) | Q(
        processing_status=ContactFormSubmission.STATUS_SCAN_RETRY,
        next_attempt_at__isnull=True,
    ) | Q(
        processing_status=ContactFormSubmission.STATUS_SCANNING,
        processing_started_at__lte=stale_before,
    )
    with transaction.atomic():
        submission = (
            ContactFormSubmission.objects.select_for_update(skip_locked=True)
            .filter(eligible)
            .order_by("created_at", "id")
            .first()
        )
        if submission is None:
            return None
        submission.processing_status = ContactFormSubmission.STATUS_SCANNING
        submission.processing_started_at = now
        submission.next_attempt_at = None
        submission.scan_attempt_count += 1
        submission.save(
            update_fields=[
                "processing_status",
                "processing_started_at",
                "next_attempt_at",
                "scan_attempt_count",
                "updated_at",
            ]
        )
        return submission


def _acquire_detection_lease(now):
    with transaction.atomic():
        state, _ = ContactSpamDetectionState.objects.get_or_create(pk=1)
        state = ContactSpamDetectionState.objects.select_for_update().get(pk=state.pk)
        if state.cooldown_until and state.cooldown_until > now:
            return "", state.cooldown_until, False
        if state.locked_until and state.locked_until > now:
            return "", None, True

        lease_seconds = max(
            int(settings.CONTACT_FORM_WORKER_LEASE_SECONDS),
            int(settings.CONTACT_SPAM_DETECTION_TIMEOUT_SECONDS) + 60,
        )
        token = uuid.uuid4().hex
        state.lock_token = token
        state.locked_until = now + timedelta(seconds=max(1, lease_seconds))
        state.cooldown_until = None
        state.save(update_fields=["lock_token", "locked_until", "cooldown_until", "updated_at"])
        return token, None, False


def _release_detection_lease(token: str, *, cooldown_until=None, error=""):
    ContactSpamDetectionState.objects.filter(pk=1, lock_token=token).update(
        lock_token="",
        locked_until=None,
        cooldown_until=cooldown_until,
        last_error=error[:4000],
        updated_at=timezone.now(),
    )


def _record_scan_failure(submission: ContactFormSubmission, error: str):
    now = timezone.now()
    retry_at = now + timedelta(seconds=max(1, int(settings.CONTACT_SPAM_DETECTION_RETRY_DELAY_SECONDS)))
    if submission.scan_attempt_count >= max(1, int(settings.CONTACT_SPAM_DETECTION_MAX_ATTEMPTS)):
        submission.processing_status = ContactFormSubmission.STATUS_FAILED
    else:
        submission.processing_status = ContactFormSubmission.STATUS_SCAN_RETRY
    submission.processing_started_at = None
    submission.next_attempt_at = retry_at
    submission.last_error = error[:4000]
    submission.save(
        update_fields=[
            "processing_status",
            "processing_started_at",
            "next_attempt_at",
            "last_error",
            "updated_at",
        ]
    )
    return retry_at


def _scan_submission(submission: ContactFormSubmission, client: SpamDetectionClient):
    result = client.classify(submission.message)
    now = timezone.now()
    submission.spam_category = result.category
    submission.spam_source = result.source
    submission.spam_checked_at = now
    submission.next_attempt_at = None
    submission.last_error = ""
    if result.category == 0:
        submission.processing_status = ContactFormSubmission.STATUS_SENDING
        submission.processing_started_at = now
    else:
        submission.processing_status = ContactFormSubmission.STATUS_BLOCKED
        submission.processing_started_at = None
    submission.save(
        update_fields=[
            "spam_category",
            "spam_source",
            "spam_checked_at",
            "processing_status",
            "processing_started_at",
            "next_attempt_at",
            "last_error",
            "updated_at",
        ]
    )
    return result


def _notification_content(submission: ContactFormSubmission, notification_settings):
    subject_prefix = (notification_settings.contact_form_subject_prefix or "").strip()
    subject = f"{subject_prefix} {submission.get_category_display()}".strip()
    escaped_message_html = escape(submission.message).replace("\n", "<br>")
    text_body = (
        f"Name: {submission.name}\n"
        f"Email: {submission.email}\n"
        f"Contact: {submission.contact}\n"
        f"Category: {submission.get_category_display()}\n"
        f"Source Page: {submission.source_page}\n"
        f"IP Address: {submission.ip_address or ''}\n\n"
        f"Message:\n{submission.message}\n"
    )
    html_body = (
        f"<p><strong>Name:</strong> {escape(submission.name)}</p>"
        f"<p><strong>Email:</strong> {escape(submission.email)}</p>"
        f"<p><strong>Contact:</strong> {escape(submission.contact)}</p>"
        f"<p><strong>Category:</strong> {escape(submission.get_category_display())}</p>"
        f"<p><strong>Source Page:</strong> {escape(submission.source_page or '')}</p>"
        f"<p><strong>IP Address:</strong> {escape(str(submission.ip_address or ''))}</p>"
        f"<p><strong>Message:</strong></p><p>{escaped_message_html}</p>"
    )
    return subject, text_body, html_body


def _user_copy_content(submission: ContactFormSubmission, notification_settings):
    values_text = {
        "name": submission.name,
        "email": submission.email,
        "contact": submission.contact,
        "category": submission.get_category_display(),
        "message": submission.message,
        "source_page": submission.source_page or "",
    }
    values_html = {
        "name": escape(submission.name),
        "email": escape(submission.email),
        "contact": escape(submission.contact),
        "category": escape(submission.get_category_display()),
        "message": escape(submission.message).replace("\n", "<br>"),
        "source_page": escape(submission.source_page or ""),
    }
    return (
        _render_contact_template(notification_settings.contact_form_user_subject_template, values_text),
        _render_contact_template(notification_settings.contact_form_user_text_template, values_text),
        _render_contact_template(notification_settings.contact_form_user_html_template, values_html),
    )


def _send_submission(submission: ContactFormSubmission):
    now = timezone.now()
    submission.send_attempt_count += 1
    submission.processing_status = ContactFormSubmission.STATUS_SENDING
    submission.processing_started_at = now
    submission.save(
        update_fields=["send_attempt_count", "processing_status", "processing_started_at", "updated_at"]
    )

    notification_settings = SystemNotificationMailSettings.load()
    smtp_settings = MailSmtpSettings.load()
    contact_settings = ContactFormSettings.load()
    try:
        if submission.notification_sent_at is None:
            subject, text_body, html_body = _notification_content(submission, notification_settings)
            send_contact_notification_email(
                subject=subject,
                text_body=text_body,
                html_body=html_body,
                notification_config=notification_settings,
                smtp_config=smtp_settings,
            )
            submission.notification_sent_at = timezone.now()
            submission.save(update_fields=["notification_sent_at", "updated_at"])

        if contact_settings.send_user_copy and submission.email and submission.user_copy_sent_at is None:
            user_subject, user_text, user_html = _user_copy_content(submission, notification_settings)
            send_contact_user_email(
                to_email=submission.email,
                subject=user_subject,
                text_body=user_text,
                html_body=user_html,
                notification_config=notification_settings,
                smtp_config=smtp_settings,
            )
            submission.user_copy_sent_at = timezone.now()
            submission.save(update_fields=["user_copy_sent_at", "updated_at"])
    except Exception as exc:
        logger.warning("contact submission email failed submission_id=%s: %s", submission.pk, exc)
        submission.last_error = str(exc)[:4000]
        submission.processing_started_at = None
        delay = max(1, int(settings.CONTACT_FORM_MAIL_RETRY_DELAY_SECONDS))
        submission.next_attempt_at = timezone.now() + timedelta(seconds=delay)
        if submission.send_attempt_count >= max(1, int(settings.CONTACT_FORM_MAIL_MAX_ATTEMPTS)):
            submission.processing_status = ContactFormSubmission.STATUS_FAILED
        else:
            submission.processing_status = ContactFormSubmission.STATUS_SEND_RETRY
        submission.save(
            update_fields=[
                "processing_status",
                "processing_started_at",
                "next_attempt_at",
                "last_error",
                "updated_at",
            ]
        )
        return False

    submission.processing_status = ContactFormSubmission.STATUS_SENT
    submission.processing_started_at = None
    submission.next_attempt_at = None
    submission.last_error = ""
    submission.save(
        update_fields=[
            "processing_status",
            "processing_started_at",
            "next_attempt_at",
            "last_error",
            "updated_at",
        ]
    )
    return True


def process_contact_submissions(*, limit=None, max_runtime_seconds=None) -> dict:
    limit = max(1, int(limit or settings.CONTACT_FORM_WORKER_BATCH_SIZE))
    max_runtime_seconds = max(
        1,
        int(max_runtime_seconds or settings.CONTACT_FORM_WORKER_MAX_RUNTIME_SECONDS),
    )
    result = {
        "processed": 0,
        "allowed": 0,
        "blocked": 0,
        "sent": 0,
        "failed": 0,
        "cooldown_until": None,
        "detector_busy": False,
        "disabled": not settings.CONTACT_SPAM_DETECTION_ENABLED,
    }
    started_at = time.monotonic()
    client = SpamDetectionClient()

    while result["processed"] < limit and time.monotonic() - started_at < max_runtime_seconds:
        now = timezone.now()
        submission = _claim_mail_submission(now)
        if submission is not None:
            result["processed"] += 1
            if _send_submission(submission):
                result["sent"] += 1
            else:
                result["failed"] += 1
            continue

        if not settings.CONTACT_SPAM_DETECTION_ENABLED:
            break

        lease_token, cooldown_until, detector_busy = _acquire_detection_lease(now)
        if not lease_token:
            result["cooldown_until"] = cooldown_until
            result["detector_busy"] = detector_busy
            break

        submission = _claim_scan_submission(now)
        if submission is None:
            _release_detection_lease(lease_token)
            break

        result["processed"] += 1
        try:
            scan_result = _scan_submission(submission, client)
        except SpamDetectionError as exc:
            logger.warning("contact spam detection failed submission_id=%s: %s", submission.pk, exc)
            retry_at = _record_scan_failure(submission, str(exc))
            _release_detection_lease(lease_token, cooldown_until=retry_at, error=str(exc))
            result["failed"] += 1
            result["cooldown_until"] = retry_at
            break

        _release_detection_lease(lease_token)

        if scan_result.category != 0:
            result["blocked"] += 1
            continue

        result["allowed"] += 1
        if _send_submission(submission):
            result["sent"] += 1
        else:
            result["failed"] += 1

    return result
