import time

from django.utils import timezone

from .models import NewsletterCampaign, NewsletterDispatchRecord, NewsletterSystemSettings
from .newsletter import (
    SendEngineClient,
    build_weekly_news_body_html,
    compose_newsletter_template_html,
    compose_newsletter_template_text,
    render_newsletter_html_for_send_job,
    render_newsletter_text_for_send_job,
)

TERMINAL_SEND_JOB_STATUSES = {"completed", "failed", "cancelled"}
SUCCESS_SEND_JOB_STATUSES = {"completed"}


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


def _is_terminal_status(value: str) -> bool:
    return _normalize_status(value) in TERMINAL_SEND_JOB_STATUSES


def _is_success_status(value: str) -> bool:
    return _normalize_status(value) in SUCCESS_SEND_JOB_STATUSES


def _build_send_job_payload(*, campaign: NewsletterCampaign, settings_obj: NewsletterSystemSettings, list_id: str) -> dict:
    site_base_url = (settings_obj.site_base_url or "").strip().rstrip("/")
    body_source_html = campaign.html_template
    if campaign.campaign_type == NewsletterCampaign.TYPE_WEEKLY_NEWS:
        config = campaign.content_config or {}
        article_ids = config.get("article_page_ids") or []
        if not article_ids:
            article_ids = list(campaign.weekly_articles.values_list("id", flat=True))
        body_source_html = build_weekly_news_body_html(
            article_page_ids=article_ids,
            item_template_html=campaign.item_template_html,
            site_base_url=site_base_url,
        )
    if campaign.newsletter_template_id and campaign.newsletter_template:
        body_source_html = compose_newsletter_template_html(
            layout_html=campaign.newsletter_template.template_html,
            email_body_html=body_source_html,
        )
    body_html = render_newsletter_html_for_send_job(body_source_html, site_base_url=site_base_url)
    body_text = (campaign.text_template or "").strip()
    if not body_text:
        body_text = render_newsletter_text_for_send_job(campaign.html_template, site_base_url=site_base_url)
    if campaign.newsletter_template_id and campaign.newsletter_template:
        source_text = (campaign.text_template or "").strip()
        if not source_text:
            source_text = render_newsletter_text_for_send_job(campaign.html_template, site_base_url=site_base_url)
        body_text = compose_newsletter_template_text(
            layout_text=campaign.newsletter_template.template_text,
            email_body_text=source_text,
        ).strip()
    subject = (campaign.subject_template or "").strip()
    if not subject and campaign.newsletter_template_id and campaign.newsletter_template:
        subject = (campaign.newsletter_template.subject or "").strip()

    payload = {
        "list_id": list_id,
        "name": campaign.title,
        "subject": subject,
    }
    tenant_id = (settings_obj.member_center_tenant_id or "").strip()
    if tenant_id:
        payload["tenant_id"] = tenant_id
    if body_html:
        payload["body_html"] = body_html
    if body_text:
        payload["body_text"] = body_text
    if site_base_url:
        one_click_path = (settings_obj.one_click_endpoint_path or "/u/unsubscribe").strip()
        if not one_click_path.startswith("/"):
            one_click_path = f"/{one_click_path}"
        unsubscribe_url_template = f"{site_base_url}{one_click_path}?token={{{{unsubscribe_token}}}}"
        payload["template"] = {
            # Send Engine uses this key to emit List-Unsubscribe headers.
            "list_unsubscribe_url_template": unsubscribe_url_template,
        }
    return payload


def _create_dispatch_record(
    *,
    campaign: NewsletterCampaign,
    status: str,
    retry_count: int,
    response_status: int | None,
    response_payload: dict,
    error_message: str = "",
):
    NewsletterDispatchRecord.objects.create(
        campaign=campaign,
        status=status,
        retry_count=max(0, int(retry_count)),
        response_status=response_status,
        response_payload=response_payload or {},
        error_message=error_message or "",
    )


def _schedule_retry(*, campaign: NewsletterCampaign, attempt: int, response_status: int, response_payload: dict, error_message: str, settings_obj: NewsletterSystemSettings):
    max_attempts = max(0, int(settings_obj.send_engine_retry_max_attempts or 0))
    interval = max(1, int(settings_obj.send_engine_retry_interval_seconds or 300))
    if attempt >= max_attempts:
        return

    NewsletterDispatchRecord.objects.create(
        campaign=campaign,
        status="retry_scheduled",
        retry_count=attempt + 1,
        next_retry_at=timezone.now() + timezone.timedelta(seconds=interval),
        response_status=response_status,
        response_payload=response_payload or {},
        error_message=error_message or "",
    )


def _poll_send_job_to_terminal(*, send_engine: SendEngineClient, send_job_id: str, poll_interval_seconds: int = 3, max_polls: int = 40) -> tuple[bool, int, dict, str]:
    history = []
    last_status = ""
    for _ in range(max(1, int(max_polls))):
        poll = send_engine.get_send_job(send_job_id)
        if not poll.ok:
            error = poll.error
            if poll.status in (401, 403):
                error = (
                    f"send engine auth failed when polling send job (HTTP {poll.status}); "
                    "token may miss scope newsletter:send.read"
                )
            return False, poll.status, {"send_job_id": send_job_id, "poll_history": history, "last_response": poll.data}, error

        status = _normalize_status(str(poll.data.get("status", "")))
        last_status = status
        history.append(
            {
                "polled_at": timezone.now().isoformat(),
                "status": status,
                "response_status": poll.status,
            }
        )
        if _is_terminal_status(status):
            return True, poll.status, {"send_job_id": send_job_id, "send_job_status": status, "poll_history": history, "send_job": poll.data}, ""

        time.sleep(max(1, int(poll_interval_seconds)))

    return (
        False,
        0,
        {"send_job_id": send_job_id, "send_job_status": last_status or "unknown", "poll_history": history},
        f"send job did not reach terminal status after {max(1, int(max_polls))} polls",
    )


def _dispatch_once(*, campaign: NewsletterCampaign, settings_obj: NewsletterSystemSettings) -> dict:
    send_engine = SendEngineClient(settings_obj)
    list_id = (campaign.list_id or settings_obj.member_center_list_id or "").strip()
    if not list_id:
        return {
            "ok": False,
            "response_status": 0,
            "response_payload": {},
            "error": "list_id is empty",
            "send_job_status": "",
        }

    payload = _build_send_job_payload(campaign=campaign, settings_obj=settings_obj, list_id=list_id)
    if not payload.get("body_html") and not payload.get("body_text") and not payload.get("template"):
        return {
            "ok": False,
            "response_status": 0,
            "response_payload": {},
            "error": "body_html/body_text/template must provide at least one value",
            "send_job_status": "",
        }

    create_result = send_engine.create_send_job(payload)
    if not create_result.ok:
        create_error = create_result.error
        if create_result.status in (401, 403):
            create_error = (
                f"send engine auth failed when creating send job (HTTP {create_result.status}); "
                "verify bearer token and scope newsletter:send.write"
            )
        return {
            "ok": False,
            "response_status": create_result.status,
            "response_payload": {"create_response": create_result.data, "payload": payload},
            "error": create_error,
            "send_job_status": "",
        }

    send_job_id = str(create_result.data.get("send_job_id") or create_result.data.get("id") or "").strip()
    create_status = _normalize_status(str(create_result.data.get("status", "")))
    response_payload = {
        "payload": payload,
        "create_response": create_result.data,
        "send_job_id": send_job_id,
        "send_job_status": create_status,
    }
    if not send_job_id:
        return {
            "ok": False,
            "response_status": create_result.status,
            "response_payload": response_payload,
            "error": "send_job_id missing from create response",
            "send_job_status": create_status,
        }

    if _is_terminal_status(create_status):
        return {
            "ok": _is_success_status(create_status),
            "response_status": create_result.status,
            "response_payload": response_payload,
            "error": "" if _is_success_status(create_status) else f"send job terminal status: {create_status}",
            "send_job_status": create_status,
        }

    poll_ok, poll_status, poll_payload, poll_error = _poll_send_job_to_terminal(
        send_engine=send_engine,
        send_job_id=send_job_id,
    )
    merged_payload = {**response_payload, **poll_payload}
    final_status = _normalize_status(str(merged_payload.get("send_job_status", "")))
    if poll_ok and _is_success_status(final_status):
        return {
            "ok": True,
            "response_status": poll_status,
            "response_payload": merged_payload,
            "error": "",
            "send_job_status": final_status,
        }

    error_message = poll_error or f"send job terminal status: {final_status or 'unknown'}"
    return {
        "ok": False,
        "response_status": poll_status,
        "response_payload": merged_payload,
        "error": error_message,
        "send_job_status": final_status,
    }


def dispatch_campaign(campaign: NewsletterCampaign, schedule_retry_on_failure: bool = True) -> dict:
    settings_obj = NewsletterSystemSettings.load()
    campaign.status = NewsletterCampaign.STATUS_SENDING
    campaign.last_error = ""
    campaign.save(update_fields=["status", "last_error", "updated_at"])

    result = _dispatch_once(campaign=campaign, settings_obj=settings_obj)
    if result["ok"]:
        campaign.sent_at = timezone.now()
        campaign.status = NewsletterCampaign.STATUS_SENT
        campaign.last_error = ""
        campaign.save(update_fields=["sent_at", "status", "last_error", "updated_at"])
        _create_dispatch_record(
            campaign=campaign,
            status="sent",
            retry_count=0,
            response_status=result["response_status"],
            response_payload=result["response_payload"],
        )
        return {"sent": 1, "failed": 0, "error": "", "send_job_status": result["send_job_status"]}

    campaign.status = NewsletterCampaign.STATUS_FAILED
    campaign.last_error = result["error"]
    campaign.save(update_fields=["status", "last_error", "updated_at"])
    _create_dispatch_record(
        campaign=campaign,
        status="failed",
        retry_count=0,
        response_status=result["response_status"],
        response_payload=result["response_payload"],
        error_message=result["error"],
    )
    if schedule_retry_on_failure:
        _schedule_retry(
            campaign=campaign,
            attempt=0,
            response_status=result["response_status"],
            response_payload=result["response_payload"],
            error_message=result["error"],
            settings_obj=settings_obj,
        )
    return {"sent": 0, "failed": 1, "error": result["error"], "send_job_status": result["send_job_status"]}


def process_due_retries(limit: int = 200) -> dict:
    settings_obj = NewsletterSystemSettings.load()
    now = timezone.now()
    due_retries = list(
        NewsletterDispatchRecord.objects.filter(
            status="retry_scheduled",
            next_retry_at__isnull=False,
            next_retry_at__lte=now,
        )
        .select_related("campaign")
        .order_by("next_retry_at", "id")[: max(1, int(limit))]
    )

    sent = 0
    failed = 0
    processed = 0

    for retry in due_retries:
        processed += 1
        campaign = retry.campaign
        retry.status = "retry_consumed"
        retry.save(update_fields=["status"])

        if campaign.status == NewsletterCampaign.STATUS_SENT:
            continue

        result = _dispatch_once(campaign=campaign, settings_obj=settings_obj)
        if result["ok"]:
            sent += 1
            campaign.sent_at = timezone.now()
            campaign.status = NewsletterCampaign.STATUS_SENT
            campaign.last_error = ""
            campaign.save(update_fields=["sent_at", "status", "last_error", "updated_at"])
            _create_dispatch_record(
                campaign=campaign,
                status="sent",
                retry_count=retry.retry_count,
                response_status=result["response_status"],
                response_payload=result["response_payload"],
            )
        else:
            failed += 1
            campaign.status = NewsletterCampaign.STATUS_FAILED
            campaign.last_error = result["error"]
            campaign.save(update_fields=["status", "last_error", "updated_at"])
            _create_dispatch_record(
                campaign=campaign,
                status="failed",
                retry_count=retry.retry_count,
                response_status=result["response_status"],
                response_payload=result["response_payload"],
                error_message=result["error"],
            )
            _schedule_retry(
                campaign=campaign,
                attempt=retry.retry_count,
                response_status=result["response_status"],
                response_payload=result["response_payload"],
                error_message=result["error"],
                settings_obj=settings_obj,
            )

    return {"processed_retries": processed, "sent_retries": sent, "failed_retries": failed}


def dispatch_due_campaigns(limit: int = 20) -> dict:
    retry_result = process_due_retries(limit=200)
    now = timezone.now()
    due_campaigns = list(
        NewsletterCampaign.objects.filter(
            status=NewsletterCampaign.STATUS_SCHEDULED,
            scheduled_at__isnull=False,
            scheduled_at__lte=now,
        )
        .order_by("scheduled_at", "id")[: max(1, int(limit))]
    )

    total_sent = 0
    total_failed = 0
    processed = 0
    for campaign in due_campaigns:
        processed += 1
        result = dispatch_campaign(campaign)
        total_sent += int(result.get("sent", 0))
        if result.get("failed", 0):
            total_failed += 1

    return {
        "processed_campaigns": processed,
        "sent_recipients": total_sent,
        "failed_campaigns": total_failed,
        **retry_result,
    }
