"""Report generator — orchestrates collection, building, validation, summarization, and storage.

Provides three public functions:
- generate_report: full pipeline from data collection to assembled ReportData
- store_report: upsert into trading_reports table
- process_report_job: Redis queue job handler with retry and dedup

Requirements: 5.1, 5.2, 5.3, 6.3, 6.4, 6.5
Design: Report Generator
"""

from __future__ import annotations

import asyncio
import logging
from datetime import date, datetime, timezone

import asyncpg

from services.reporting.collector import collect_report_data
from services.reporting.models import ReportData, ReportType
from services.reporting.sections import (
    build_model_quality_section,
    build_pnl_section,
    build_position_performance_section,
    build_recommendation_accuracy_section,
    build_risk_metrics_section,
)
from services.reporting.summarizer import (
    generate_executive_summary,
    summarize_section,
)
from services.reporting.validator import (
    compute_validation_status,
    validate_model_quality,
    validate_recommendation_accuracy,
)
from services.shared.agent_config import AgentConfigResolver

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Retry configuration for process_report_job
# ---------------------------------------------------------------------------

_MAX_RETRIES = 3
_BACKOFF_SECONDS = (30, 60, 120)

# In-memory set tracking in-progress jobs to reject duplicates.
# Key format: "{report_type}:{period_start}:{period_end}"
_in_progress_jobs: set[str] = set()


# ---------------------------------------------------------------------------
# generate_report
# ---------------------------------------------------------------------------


async def generate_report(
    pool: asyncpg.Pool,
    report_type: ReportType,
    period_start: date,
    period_end: date,
) -> ReportData:
    """Orchestrate full report generation.

    1. Collect data via collector
    2. Build all 5 sections via section builders
    3. Validate recommendation_accuracy and model_quality via validator
    4. Create AgentConfigResolver and summarize each section
    5. Generate executive summary
    6. Assemble final ReportData
    """
    # 1. Collect data
    data = await collect_report_data(pool, period_start, period_end)

    # 2. Build sections
    pnl = build_pnl_section(data)
    rec_accuracy = build_recommendation_accuracy_section(data)
    position_perf = build_position_performance_section(data)
    risk_metrics = build_risk_metrics_section(data)
    model_quality = build_model_quality_section(data)

    # 3. Validate
    rec_warnings = validate_recommendation_accuracy(
        rec_accuracy, data.prediction_outcomes,
    )
    rec_accuracy.validation_warnings = rec_warnings

    mq_warnings = validate_model_quality(
        model_quality, data.model_metric_snapshots,
    )
    model_quality.validation_warnings = mq_warnings

    # 4. Summarize each section
    resolver = AgentConfigResolver(pool)

    pnl.summary = await summarize_section(
        pool, resolver, "pnl", pnl.model_dump(),
    )
    rec_accuracy.summary = await summarize_section(
        pool, resolver, "recommendation_accuracy", rec_accuracy.model_dump(),
    )
    position_perf.summary = await summarize_section(
        pool, resolver, "position_performance", position_perf.model_dump(),
    )
    risk_metrics.summary = await summarize_section(
        pool, resolver, "risk_metrics", risk_metrics.model_dump(),
    )
    model_quality.summary = await summarize_section(
        pool, resolver, "model_quality", model_quality.model_dump(),
    )

    # 5. Generate executive summary
    section_summaries = {
        "pnl": pnl.summary,
        "recommendation_accuracy": rec_accuracy.summary,
        "position_performance": position_perf.summary,
        "risk_metrics": risk_metrics.summary,
        "model_quality": model_quality.summary,
    }
    executive_summary = await generate_executive_summary(
        pool, resolver, section_summaries,
    )

    # 6. Assemble ReportData
    report = ReportData(
        pnl=pnl,
        recommendation_accuracy=rec_accuracy,
        position_performance=position_perf,
        risk_metrics=risk_metrics,
        model_quality=model_quality,
        executive_summary=executive_summary,
        generated_at=datetime.now(timezone.utc),
        period_start=period_start,
        period_end=period_end,
        report_type=ReportType(report_type),
    )

    # Set validation status based on all warnings
    report.validation_status = compute_validation_status(report)

    return report


# ---------------------------------------------------------------------------
# store_report
# ---------------------------------------------------------------------------

_UPSERT_SQL = """\
INSERT INTO trading_reports
    (report_type, period_start, period_end, report_data, validation_status, generated_at)
VALUES
    ($1, $2, $3, $4::jsonb, $5, $6)
ON CONFLICT (report_type, period_start, period_end)
DO UPDATE SET
    report_data = EXCLUDED.report_data,
    validation_status = EXCLUDED.validation_status,
    generated_at = EXCLUDED.generated_at
RETURNING id
"""


async def store_report(
    pool: asyncpg.Pool,
    report: ReportData,
) -> str:
    """Store report in trading_reports table via upsert.

    Uses INSERT ... ON CONFLICT (report_type, period_start, period_end)
    DO UPDATE to handle regeneration of existing reports.

    Returns the report UUID as a string.
    """
    row = await pool.fetchrow(
        _UPSERT_SQL,
        report.report_type.value,
        report.period_start,
        report.period_end,
        report.model_dump_json(),
        report.validation_status.value,
        report.generated_at,
    )
    report_id = str(row["id"])  # type: ignore[index]
    logger.info(
        "Stored report %s (type=%s, period=%s to %s)",
        report_id,
        report.report_type.value,
        report.period_start,
        report.period_end,
    )
    return report_id


# ---------------------------------------------------------------------------
# process_report_job
# ---------------------------------------------------------------------------


def _job_key(report_type: str, period_start: str, period_end: str) -> str:
    """Build a dedup key for an in-progress job."""
    return f"{report_type}:{period_start}:{period_end}"


async def process_report_job(
    pool: asyncpg.Pool,
    job: dict,
) -> None:
    """Process a report generation job from the Redis queue.

    Deserializes job payload, calls generate_report + store_report.
    Handles retries with exponential backoff (30s, 60s, 120s up to 3 attempts).
    Rejects duplicate jobs for the same report_type + period.

    Expected job payload::

        {
            "report_type": "daily" | "weekly",
            "period_start": "YYYY-MM-DD",
            "period_end": "YYYY-MM-DD"
        }
    """
    report_type_str = job.get("report_type", "")
    period_start_str = job.get("period_start", "")
    period_end_str = job.get("period_end", "")

    # Validate payload
    try:
        report_type = ReportType(report_type_str)
        period_start = date.fromisoformat(period_start_str)
        period_end = date.fromisoformat(period_end_str)
    except (ValueError, TypeError) as exc:
        logger.error("Invalid report job payload: %s — %s", job, exc)
        return

    # Reject duplicate in-progress jobs
    key = _job_key(report_type_str, period_start_str, period_end_str)
    if key in _in_progress_jobs:
        logger.warning(
            "Duplicate report job rejected (already in progress): %s", key,
        )
        return

    _in_progress_jobs.add(key)
    try:
        last_error: Exception | None = None
        for attempt in range(_MAX_RETRIES):
            try:
                report = await generate_report(
                    pool, report_type, period_start, period_end,
                )
                await store_report(pool, report)
                logger.info(
                    "Report job completed: %s (attempt %d)", key, attempt + 1,
                )
                return
            except Exception as exc:
                last_error = exc
                if attempt < _MAX_RETRIES - 1:
                    backoff = _BACKOFF_SECONDS[attempt]
                    logger.warning(
                        "Report job %s failed (attempt %d/%d): %s — retrying in %ds",
                        key,
                        attempt + 1,
                        _MAX_RETRIES,
                        exc,
                        backoff,
                    )
                    await asyncio.sleep(backoff)

        # All retries exhausted
        logger.error(
            "Report job %s failed after %d attempts: %s",
            key,
            _MAX_RETRIES,
            last_error,
        )
    finally:
        _in_progress_jobs.discard(key)
