"""Reporting Engine — Celery tasks.

``process_report_execution``
    Main worker task for executing a report asynchronously.  Fetches data,
    applies filters, formats output (CSV/XLSX/PDF), saves to storage, and
    transitions the execution status.

``tick_scheduled_reports``
    Periodic task (Celery Beat).  Finds due ``ScheduledReport`` rows and
    dispatches ``process_report_execution`` for each.

``compute_next_run``
    Pure helper: given a cron expression returns the next UTC datetime.
"""

from __future__ import annotations

import csv
import datetime
import io
import json
from typing import Any

import structlog
from celery import shared_task
from django.utils import timezone

_log = structlog.get_logger("simorgh.reporting.tasks")


# ---------------------------------------------------------------------------
# Cron helper
# ---------------------------------------------------------------------------

def compute_next_run(
    cron_expression: str,
    timezone_name: str = "UTC",
    base: datetime.datetime | None = None,
) -> datetime.datetime:
    """Return the next UTC datetime for *cron_expression* after *base*."""
    import croniter
    import pytz

    tz = pytz.timezone(timezone_name)
    if base is None:
        base = timezone.now()
    local_base = base.astimezone(tz)
    cron = croniter.croniter(cron_expression, local_base)
    next_local = cron.get_next(datetime.datetime)
    return next_local.astimezone(pytz.UTC)


# ---------------------------------------------------------------------------
# Report execution
# ---------------------------------------------------------------------------

@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def process_report_execution(self, execution_id: int) -> dict[str, Any]:
    """Execute a report: fetch data, format, save output, update status.

    Retries up to 3 times with 60s delay on transient failures.
    """
    from simorgh.apps.reporting.models import ReportExecution, ReportExecutionStatus

    try:
        execution = ReportExecution.objects.select_related("report").get(pk=execution_id)
    except ReportExecution.DoesNotExist:
        _log.error("reporting.execution.not_found", execution_id=execution_id)
        return {"status": "not_found"}

    if execution.status not in (ReportExecutionStatus.PENDING, ReportExecutionStatus.FAILED):
        _log.info(
            "reporting.execution.already_processed",
            execution_id=execution_id,
            status=execution.status,
        )
        return {"status": execution.status}

    execution.mark_started()

    try:
        report = execution.report
        if report is None:
            raise ValueError("Report definition has been deleted.")

        rows = _fetch_report_data(execution)
        file_metadata = _generate_output_file(execution, rows)
        execution.mark_completed(
            row_count=len(rows),
            file_id=file_metadata.pk if file_metadata else None,
        )

        _log.info(
            "reporting.execution.completed",
            execution_id=execution_id,
            report_code=report.code,
            row_count=len(rows),
        )

        # Dispatch event
        from simorgh.apps.events.bus import dispatch
        dispatch(
            "reporting.report_executed",
            {
                "execution_id": execution.pk,
                "report_code": report.code,
                "report_name": report.name,
                "tenant_id": execution.tenant_id,
                "row_count": len(rows),
                "format": execution.format,
                "triggered_by": execution.triggered_by_id,
            },
        )

        return {"status": "completed", "row_count": len(rows)}

    except Exception as exc:
        _log.exception(
            "reporting.execution.failed",
            execution_id=execution_id,
            error=str(exc),
        )
        execution.mark_failed(error_message=str(exc))

        if self.request.retries < self.max_retries:
            raise self.retry(exc=exc) from exc

        return {"status": "failed", "error": str(exc)}


def _fetch_report_data(execution) -> list[dict[str, Any]]:
    """Fetch report data from the report's resource endpoint.

    In a real deployment this would call the configured API endpoint.
    For now, it constructs a queryset-based fetch via the export registry
    or falls back to an empty result set.
    """
    from simorgh.apps.platform_core.export_registry import export_registry

    report = execution.report
    if not report:
        return []

    # If the report maps to an ExportSpec, use that for data retrieval.
    if report.entity_type and report.entity_type in export_registry:
        spec = export_registry.get(report.entity_type)
        if spec is not None:
            queryset = spec.queryset_fn(
                tenant=execution.tenant,
                actor=execution.triggered_by,
                filters=execution.filters_applied,
            )
            return [spec.row_fn(obj) for obj in queryset]

    # Fallback: return empty result — modules must register ExportSpecs
    # or override this function with a custom data fetcher.
    _log.warning(
        "reporting.execution.no_data_source",
        execution_id=execution.pk,
        entity_type=report.entity_type,
    )
    return []


def _generate_output_file(execution, rows: list[dict[str, Any]]):
    """Generate the output file in the requested format and save to storage.

    Returns a ``FileMetadata`` instance, or None if no rows or unsupported format.
    """
    if not rows:
        return None

    fmt = execution.format.lower() if execution.format else "csv"

    if fmt == "csv":
        return _generate_csv(execution, rows)
    elif fmt == "json":
        return _generate_json(execution, rows)
    elif fmt == "xlsx":
        return _generate_xlsx(execution, rows)
    elif fmt == "pdf":
        return _generate_pdf(execution, rows)
    else:
        _log.warning("reporting.execution.unsupported_format", format=fmt)
        return None


def _generate_csv(execution, rows: list[dict[str, Any]]):
    """Generate a CSV file from report rows."""
    from django.core.files.base import ContentFile

    from simorgh.apps.storage.models import FileMetadata

    report = execution.report
    if not rows:
        return None

    # Derive column keys from the report definition or the first row.
    if report and report.columns:
        column_keys = [c["key"] for c in report.columns if isinstance(c, dict)]
    else:
        column_keys = list(rows[0].keys())

    buf = io.StringIO()
    writer = csv.DictWriter(buf, fieldnames=column_keys, extrasaction="ignore")
    writer.writeheader()
    writer.writerows(rows)

    content = buf.getvalue()
    buf.close()

    filename = f"report_{report.code if report else 'export'}_{execution.pk}.csv"
    content_file = ContentFile(content.encode("utf-8"), name=filename)

    file_meta = FileMetadata.objects.create(
        tenant=execution.tenant,
        original_filename=filename,
        file=content_file,
        content_type="text/csv",
        size_bytes=len(content.encode("utf-8")),
    )
    return file_meta


def _generate_json(execution, rows: list[dict[str, Any]]):
    """Generate a JSON file from report rows."""
    from django.core.files.base import ContentFile

    from simorgh.apps.storage.models import FileMetadata

    report = execution.report
    content = json.dumps(rows, indent=2, ensure_ascii=False, default=str)
    filename = f"report_{report.code if report else 'export'}_{execution.pk}.json"
    content_file = ContentFile(content.encode("utf-8"), name=filename)

    file_meta = FileMetadata.objects.create(
        tenant=execution.tenant,
        original_filename=filename,
        file=content_file,
        content_type="application/json",
        size_bytes=len(content.encode("utf-8")),
    )
    return file_meta


def _generate_xlsx(execution, rows: list[dict[str, Any]]):
    """Generate an XLSX file from report rows.

    Requires ``openpyxl`` to be installed.
    """
    try:
        import openpyxl
    except ImportError:
        _log.warning("reporting.execution.xlsx_missing_openpyxl")
        return None

    from django.core.files.base import ContentFile

    from simorgh.apps.storage.models import FileMetadata

    report = execution.report

    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = report.code if report else "Report"

    if report and report.columns:
        column_keys = [c["key"] for c in report.columns if isinstance(c, dict)]
        column_labels = [
            c.get("label", c["key"]) for c in report.columns if isinstance(c, dict)
        ]
    elif rows:
        column_keys = list(rows[0].keys())
        column_labels = column_keys
    else:
        wb.close()
        return None

    ws.append(column_labels)
    for row in rows:
        ws.append([row.get(k, "") for k in column_keys])

    buf = io.BytesIO()
    wb.save(buf)
    wb.close()
    buf.seek(0)

    filename = f"report_{report.code if report else 'export'}_{execution.pk}.xlsx"
    content_file = ContentFile(buf.read(), name=filename)

    file_meta = FileMetadata.objects.create(
        tenant=execution.tenant,
        original_filename=filename,
        file=content_file,
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        size_bytes=buf.tell(),
    )
    return file_meta


def _generate_pdf(execution, rows: list[dict[str, Any]]):
    """Generate a PDF file from report rows.

    Requires ``xhtml2pdf`` to be installed.
    """
    try:
        from xhtml2pdf import pisa
    except ImportError:
        _log.warning("reporting.execution.pdf_missing_xhtml2pdf")
        return None

    from django.core.files.base import ContentFile

    from simorgh.apps.storage.models import FileMetadata

    report = execution.report

    # Build a simple HTML table.
    if report and report.columns:
        column_keys = [c["key"] for c in report.columns if isinstance(c, dict)]
        column_labels = [
            c.get("label", c["key"]) for c in report.columns if isinstance(c, dict)
        ]
    elif rows:
        column_keys = list(rows[0].keys())
        column_labels = column_keys
    else:
        return None

    html = f"""<html><head><meta charset="utf-8"><style>
        body {{ font-family: Arial, sans-serif; }}
        table {{ border-collapse: collapse; width: 100%; }}
        th, td {{ border: 1px solid #ccc; padding: 6px 10px; text-align: left; }}
        th {{ background-color: #f5f5f5; }}
        </style></head><body>
        <h1>{report.name if report else "Report"}</h1>
        <table><thead><tr>{''.join(f'<th>{lbl}</th>' for lbl in column_labels)}</tr></thead>
        <tbody>
        {''.join('<tr>' + ''.join(f'<td>{row.get(k, "")}</td>' for k in column_keys) + '</tr>' for row in rows)}
        </tbody></table></body></html>"""

    buf = io.BytesIO()
    pisa.CreatePDF(html, dest=buf, encoding="utf-8")
    buf.seek(0)

    filename = f"report_{report.code if report else 'export'}_{execution.pk}.pdf"
    content_file = ContentFile(buf.read(), name=filename)

    file_meta = FileMetadata.objects.create(
        tenant=execution.tenant,
        original_filename=filename,
        file=content_file,
        content_type="application/pdf",
        size_bytes=buf.tell(),
    )
    return file_meta


# ---------------------------------------------------------------------------
# Scheduled report ticker
# ---------------------------------------------------------------------------

@shared_task
def tick_scheduled_reports() -> dict[str, Any]:
    """Periodic task: dispatch due scheduled reports.

    Called by Celery Beat every 60 seconds.
    """
    from simorgh.apps.reporting.selectors import get_due_schedules

    due = list(get_due_schedules())
    dispatched = 0

    for schedule in due:
        try:
            execution = _dispatch_scheduled_report(schedule)
            dispatched += 1
            _log.info(
                "reporting.schedule.dispatched",
                schedule_id=schedule.pk,
                report_code=schedule.report.code,
                execution_id=execution.pk,
            )
        except Exception as exc:
            _log.error(
                "reporting.schedule.dispatch_failed",
                schedule_id=schedule.pk,
                error=str(exc),
            )

    return {"dispatched": dispatched, "checked": len(due)}


def _dispatch_scheduled_report(schedule):
    """Dispatch a single scheduled report and advance ``next_run_at``."""
    from django.utils import timezone

    from simorgh.apps.reporting.models import ReportExecution, ReportExecutionStatus

    execution = ReportExecution.objects.create(
        tenant=schedule.tenant,
        organization_node=schedule.organization_node,
        report=schedule.report,
        schedule=schedule,
        status=ReportExecutionStatus.PENDING,
        format=schedule.format,
    )

    try:
        process_report_execution.delay(execution.pk)
    except Exception:
        execution.mark_failed(error_message="Failed to dispatch Celery task")
        raise

    # Advance schedule's next_run_at.
    schedule.last_run_at = timezone.now()
    schedule.next_run_at = compute_next_run(
        schedule.cron_expression,
        base=schedule.last_run_at,
    )
    schedule.run_count = (schedule.run_count or 0) + 1
    schedule.save(update_fields=["last_run_at", "next_run_at", "run_count"])

    return execution
