"""Celery tasks for scheduled report generation and distribution."""
from __future__ import annotations

import logging

import requests
from celery import shared_task

logger = logging.getLogger(__name__)


@shared_task(name="apps.mel.reports.generate_scheduled_reports")
def generate_scheduled_reports_task() -> int:
    """Generate + publish + distribute one report per active template.

    Uses the current quarter as the reporting window. Templates declaring a
    ``schedule_cron`` are still free to override via django-celery-beat —
    this task is a safety net and can be pointed at any desired cadence in
    the periodic task configuration.
    """
    from apps.mel.reports.models import ReportTemplate
    from apps.mel.reports.services import (
        generate_render_and_publish,
        quarter_window,
    )

    label, start, end = quarter_window()
    count = 0
    for template in ReportTemplate.objects.filter(is_active=True):
        try:
            generate_render_and_publish(
                template=template,
                period_label=label,
                period_start=start,
                period_end=end,
            )
            count += 1
        except Exception:  # pragma: no cover - defensive
            logger.exception("mel.report scheduled generation failed for %s", template.slug)
    logger.info("mel.report scheduled run generated=%s", count)
    return count


@shared_task(name="apps.mel.reports.run_scheduled_reports")
def run_scheduled_reports_task() -> int:
    """Fire any :class:`ScheduledReport` rows whose ``next_run_at`` has passed.

    Designed to be invoked every minute by celery beat. Each fire creates a
    Report through ``generate_render_and_publish`` then advances ``next_run_at``
    according to ``frequency`` (cron expressions are deferred to django-celery-beat
    in v1).
    """
    from datetime import timedelta

    from django.utils import timezone

    from apps.mel.reports.models import ScheduledReport, ScheduleFrequency
    from apps.mel.reports.services import (
        generate_render_and_publish,
        quarter_window,
    )

    now = timezone.now()
    fired = 0
    for sched in ScheduledReport.objects.filter(
        is_active=True,
    ).filter(next_run_at__isnull=True) | ScheduledReport.objects.filter(
        is_active=True, next_run_at__lte=now,
    ):
        # Avoid duplicate firing within the same iteration.
        if sched.last_run_at and (now - sched.last_run_at).total_seconds() < 30:
            continue
        try:
            label, start, end = quarter_window()
            generate_render_and_publish(
                template=sched.template,
                period_label=label,
                period_start=start,
                period_end=end,
                formats=list(sched.formats or []) or None,
            )
            sched.last_run_at = now
            sched.next_run_at = _advance(now, sched.frequency)
            sched.save(update_fields=["last_run_at", "next_run_at", "updated_at"])
            fired += 1
        except Exception:  # pragma: no cover - defensive
            logger.exception("scheduled report fire failed pk=%s", sched.pk)
    if fired:
        logger.info("mel.report scheduled fired=%s", fired)
    return fired


def _advance(now, frequency: str):
    from datetime import timedelta

    if frequency == "daily":
        return now + timedelta(days=1)
    if frequency == "weekly":
        return now + timedelta(days=7)
    if frequency == "monthly":
        return now + timedelta(days=30)
    if frequency == "annual":
        return now + timedelta(days=365)
    return now + timedelta(days=90)


@shared_task(name="apps.mel.reports.distribute_report")
def distribute_report_async(report_id: int) -> int:
    from apps.mel.reports.models import Report
    from apps.mel.reports.services import distribute_report

    report = Report.objects.get(pk=report_id)
    return distribute_report(report)


@shared_task(name="apps.mel.reports.run_compliance_scan_nightly")
def run_compliance_scan_nightly_task() -> int:
    """Re-evaluate compliance checks on all published reports."""
    from apps.mel.reports.models import Report, ReportStatus
    from apps.mel.reports.services import run_compliance_checks

    total = 0
    for report in Report.objects.filter(status=ReportStatus.PUBLISHED):
        total += len(run_compliance_checks(report))
    logger.info("mel.report compliance scan created=%s", total)
    return total


# ---------------------------------------------------------------------------
# G18 — Webhook delivery
# ---------------------------------------------------------------------------


@shared_task(
    bind=True,
    name="apps.mel.reports.deliver_webhook",
    autoretry_for=(Exception,),
    retry_backoff=True,
    retry_jitter=True,
    max_retries=5,
)
def deliver_webhook(self, webhook_id: int, report_id: int) -> str:
    """POST the published-report payload to one webhook endpoint.

    Idempotent on (webhook, report) — `get_or_create` ensures a single
    delivery row per pair, even on retry. Body is HMAC-SHA256-signed with
    the webhook's secret; receivers verify via the ``X-IILMP-Signature``
    header.
    """
    import hashlib
    import hmac
    import json as _json

    from apps.mel.reports.models import (
        Report,
        ReportDistributionWebhook,
        ReportWebhookDelivery,
        WebhookDeliveryStatus,
    )

    webhook = ReportDistributionWebhook.objects.get(pk=webhook_id)
    report = Report.objects.select_related("template").get(pk=report_id)
    delivery, _ = ReportWebhookDelivery.objects.get_or_create(
        webhook=webhook, report=report,
    )

    share_url = ""
    if report.share_token:
        # The PublicReportView is at /m/r/reports/share/<token>/ — full URL
        # reconstruction requires SITE_DOMAIN; defer to the slug-only path.
        share_url = f"/m/r/reports/share/{report.share_token}/"

    payload = {
        "event": "report.published",
        "report_id": report.pk,
        "template": report.template.name if report.template else "",
        "period_label": report.period_label,
        "share_url": share_url,
        "summary": report.consolidated_data or {},
    }
    body = _json.dumps(payload, default=str).encode("utf-8")
    signature = hmac.new(
        webhook.secret.encode("utf-8"), body, hashlib.sha256,
    ).hexdigest()
    headers = {
        "Content-Type": "application/json",
        "X-IILMP-Signature": f"sha256={signature}",
        "X-IILMP-Event": "report.published",
    }

    from django.utils import timezone as _tz

    delivery.attempts += 1
    delivery.last_attempted_at = _tz.now()

    try:
        resp = requests.post(webhook.url, data=body, headers=headers, timeout=10)
        delivery.last_response_status = resp.status_code
        delivery.last_response_excerpt = (resp.text or "")[:2000]
        if 200 <= resp.status_code < 300:
            delivery.status = WebhookDeliveryStatus.SUCCESS
            delivery.save(update_fields=[
                "status", "attempts", "last_response_status",
                "last_response_excerpt", "last_attempted_at",
            ])
            logger.info("mel.webhook delivered hook=%s report=%s", webhook_id, report_id)
            return "ok"
        # 5xx triggers retry; 4xx is permanent failure.
        delivery.status = WebhookDeliveryStatus.FAILED
        delivery.save(update_fields=[
            "status", "attempts", "last_response_status",
            "last_response_excerpt", "last_attempted_at",
        ])
        if resp.status_code >= 500:
            raise Exception(f"upstream {resp.status_code}")
        logger.warning(
            "mel.webhook permanent-failure hook=%s report=%s status=%s",
            webhook_id, report_id, resp.status_code,
        )
        return f"failed:{resp.status_code}"
    except Exception as exc:
        delivery.last_response_excerpt = str(exc)[:2000]
        delivery.status = WebhookDeliveryStatus.FAILED
        delivery.save(update_fields=[
            "status", "attempts", "last_response_excerpt", "last_attempted_at",
        ])
        raise
