"""Custom report service for aggregating structured report data.

This service handles business logic for querying and aggregating custom report
data extracted from bugs with severity "custom". It replaces manual Excel
pivot table workflows by providing auto-detected field types and statistics.

Responsibilities:
- Query custom reports by test IDs
- Auto-detect field types (numeric, categorical, text)
- Compute per-field statistics (avg, min, max, distribution for numeric;
  value counts for categorical; count for text)
- Support group_by cross-tabulation
"""

import json
import logging
from collections import defaultdict
from typing import Any

from sqlmodel import col, func, select
from sqlmodel.ext.asyncio.session import AsyncSession

from testio_mcp.models.orm.bug import Bug
from testio_mcp.models.orm.custom_report import CustomReport

logger = logging.getLogger(__name__)

# Thresholds for auto-detection
NUMERIC_THRESHOLD = 0.8  # >80% parseable as float → numeric
MAX_CATEGORICAL_UNIQUE = 20  # <=20 unique values → categorical

# System dimensions: group_by values that map to JOINed DB columns,
# not to fields within report_data. Injected as metadata into parsed
# reports and excluded from field summaries to prevent misclassification.
SYSTEM_DIMENSIONS: frozenset[str] = frozenset({"test"})


class CustomReportService:
    """Business logic for custom report aggregation.

    Framework-agnostic: can be used from MCP tools, REST APIs, or CLI.
    """

    __test__ = False

    def __init__(self, session: AsyncSession, customer_id: int) -> None:
        self.session = session
        self.customer_id = customer_id

    async def get_summary(
        self,
        test_ids: list[int],
        group_by: list[str] | None = None,
        fields: list[str] | None = None,
    ) -> dict[str, Any]:
        """Aggregate custom report fields across test cycles.

        For each field, auto-detects type:
        - Numeric (>80% parseable as float): avg, min, max, distribution
        - Categorical (<=20 unique values): value counts
        - Text (everything else): count only

        Args:
            test_ids: Test IDs to query reports for (required)
            group_by: Fields to group by for cross-tabulation (max 2)
            fields: Specific fields to include (defaults to all)

        Returns:
            Structured summary suitable for report generation.
        """
        # Check if group_by includes system dimensions (e.g., "test")
        active_system_dims = SYSTEM_DIMENSIONS & set(group_by) if group_by else frozenset()

        # Query all custom reports for the given test IDs.
        # When system dimensions are active, also SELECT the mapped DB column
        # so we can inject it into parsed reports for grouping.
        parsed_reports: list[dict[str, str]] = []
        configuration_ids: set[int] = set()

        if active_system_dims:
            stmt_with_test = (
                select(CustomReport, Bug.test_id)
                .join(Bug, CustomReport.bug_id == Bug.id)  # type: ignore[arg-type]
                .where(
                    col(Bug.test_id).in_(test_ids),
                    Bug.customer_id == self.customer_id,
                )
            )
            result_with_test = await self.session.exec(stmt_with_test)
            for report, test_id in result_with_test.all():
                try:
                    data = json.loads(report.report_data)
                    if "test" in active_system_dims:
                        data["test"] = str(test_id)
                    parsed_reports.append(data)
                    if report.configuration_id is not None:
                        configuration_ids.add(report.configuration_id)
                except (json.JSONDecodeError, TypeError):
                    continue
        else:
            stmt_simple = (
                select(CustomReport)
                .join(Bug, CustomReport.bug_id == Bug.id)  # type: ignore[arg-type]
                .where(
                    col(Bug.test_id).in_(test_ids),
                    Bug.customer_id == self.customer_id,
                )
            )
            result_simple = await self.session.exec(stmt_simple)
            for report in result_simple.all():
                try:
                    data = json.loads(report.report_data)
                    parsed_reports.append(data)
                    if report.configuration_id is not None:
                        configuration_ids.add(report.configuration_id)
                except (json.JSONDecodeError, TypeError):
                    continue

        if not parsed_reports:
            return {
                "test_ids": test_ids,
                "total_reports": 0,
                "field_summary": {},
            }

        # Build result
        result_dict: dict[str, Any] = {
            "test_ids": test_ids,
            "total_reports": len(parsed_reports),
        }

        # Include configuration_id if all reports share the same one
        if len(configuration_ids) == 1:
            result_dict["configuration_id"] = configuration_ids.pop()

        if group_by:
            result_dict["group_by"] = group_by
            result_dict["groups"] = self._compute_grouped(parsed_reports, group_by, fields)
        else:
            result_dict["field_summary"] = self._compute_field_summary(parsed_reports, fields)

        return result_dict

    async def list_reports(
        self,
        test_ids: list[int],
        fields: list[str] | None = None,
        page: int = 1,
        per_page: int = 100,
    ) -> dict[str, Any]:
        """List individual custom report records with pagination.

        Returns non-aggregated report rows with bug context, suitable for
        building per-report prompt files in executive summary generation.

        Args:
            test_ids: Test IDs to query reports for (required)
            fields: Specific report fields to include (defaults to all)
            page: Page number (1-indexed)
            per_page: Items per page (default: 100)

        Returns:
            Dictionary with reports list and pagination metadata.
        """
        # Count query for pagination total
        count_stmt = (
            select(func.count(CustomReport.bug_id))  # type: ignore[arg-type]
            .join(Bug, CustomReport.bug_id == Bug.id)  # type: ignore[arg-type]
            .where(
                col(Bug.test_id).in_(test_ids),
                Bug.customer_id == self.customer_id,
            )
        )
        count_result = await self.session.exec(count_stmt)
        total_count: int = count_result.one()

        if total_count == 0:
            return {
                "test_ids": test_ids,
                "total_reports": 0,
                "reports": [],
                "pagination": {
                    "page": page,
                    "per_page": per_page,
                    "total_count": 0,
                    "has_more": False,
                },
            }

        # Data query: CustomReport + Bug.id + Bug.title
        stmt = (
            select(CustomReport, Bug.id, Bug.title)
            .join(Bug, CustomReport.bug_id == Bug.id)  # type: ignore[arg-type]
            .where(
                col(Bug.test_id).in_(test_ids),
                Bug.customer_id == self.customer_id,
            )
            .order_by(col(Bug.id))
            .offset((page - 1) * per_page)
            .limit(per_page)
        )

        result = await self.session.exec(stmt)
        rows = result.all()

        reports: list[dict[str, Any]] = []
        for row in rows:
            report, bug_id, bug_title = row
            try:
                data = json.loads(report.report_data)
            except (json.JSONDecodeError, TypeError):
                continue

            # Filter fields if specified
            if fields:
                data = {k: v for k, v in data.items() if k in fields}

            reports.append(
                {
                    "bug_id": bug_id,
                    "title": bug_title,
                    "report_fields": data,
                }
            )

        offset = (page - 1) * per_page
        has_more = offset + len(reports) < total_count

        return {
            "test_ids": test_ids,
            "total_reports": total_count,
            "reports": reports,
            "pagination": {
                "page": page,
                "per_page": per_page,
                "total_count": total_count,
                "has_more": has_more,
            },
        }

    def _compute_field_summary(
        self,
        reports: list[dict[str, str]],
        fields: list[str] | None = None,
    ) -> dict[str, Any]:
        """Compute statistics for each field across all reports."""
        # Collect all values per key
        values_by_key: dict[str, list[str]] = defaultdict(list)
        for report in reports:
            for key, value in report.items():
                if key in SYSTEM_DIMENSIONS:
                    continue  # Skip system metadata injected for grouping
                if fields and key not in fields:
                    continue
                values_by_key[key].append(value)

        summary: dict[str, Any] = {}
        for key, values in sorted(values_by_key.items()):
            summary[key] = self._classify_and_summarize(values)

        return summary

    def _classify_and_summarize(self, values: list[str]) -> dict[str, Any]:
        """Auto-detect field type and compute appropriate statistics."""
        count = len(values)

        # Try numeric classification
        numeric_values: list[float] = []
        for v in values:
            try:
                numeric_values.append(float(v))
            except (ValueError, TypeError):
                pass

        if count > 0 and len(numeric_values) / count >= NUMERIC_THRESHOLD:
            return self._numeric_summary(numeric_values, count)

        # Try categorical classification
        unique_values = set(values)
        if len(unique_values) <= MAX_CATEGORICAL_UNIQUE:
            return self._categorical_summary(values, count)

        # Fallback: text
        return {"type": "text", "count": count}

    def _numeric_summary(self, values: list[float], total_count: int) -> dict[str, Any]:
        """Compute numeric statistics with auto-generated distribution."""
        avg = sum(values) / len(values)
        min_val = min(values)
        max_val = max(values)

        # Build distribution
        # Check if all values are integers in a small range
        int_values = [int(v) for v in values if v == int(v)]
        distinct_ints = set(int_values)

        if len(int_values) == len(values) and len(distinct_ints) <= 10:
            # Use exact value counts for small integer ranges
            distribution: dict[str, int] = {}
            for v in sorted(distinct_ints):
                distribution[str(v)] = int_values.count(v)
        else:
            # Generate even bucket boundaries
            distribution = self._bucket_distribution(values, min_val, max_val)

        result: dict[str, Any] = {
            "type": "numeric",
            "count": total_count,
            "avg": round(avg, 2),
            "min": round(min_val, 2),
            "max": round(max_val, 2),
            "distribution": distribution,
        }

        return result

    def _bucket_distribution(
        self, values: list[float], min_val: float, max_val: float
    ) -> dict[str, int]:
        """Generate even bucket boundaries for numeric distribution."""
        if min_val == max_val:
            return {str(round(min_val, 2)): len(values)}

        # Aim for ~5 buckets
        num_buckets = min(5, len(set(values)))
        if num_buckets < 2:
            return {str(round(min_val, 2)): len(values)}

        bucket_size = (max_val - min_val) / num_buckets
        distribution: dict[str, int] = {}

        for i in range(num_buckets):
            lower = min_val + i * bucket_size
            upper = min_val + (i + 1) * bucket_size
            if i == num_buckets - 1:
                # Last bucket includes upper bound
                count = sum(1 for v in values if lower <= v <= upper)
                label = f"{round(lower, 1)}-{round(upper, 1)}"
            else:
                count = sum(1 for v in values if lower <= v < upper)
                label = f"{round(lower, 1)}-{round(upper, 1)}"
            if count > 0:
                distribution[label] = count

        return distribution

    def _categorical_summary(self, values: list[str], count: int) -> dict[str, Any]:
        """Compute categorical value counts."""
        value_counts: dict[str, int] = {}
        for v in values:
            value_counts[v] = value_counts.get(v, 0) + 1

        # Sort by count descending
        sorted_counts = dict(sorted(value_counts.items(), key=lambda x: x[1], reverse=True))

        return {
            "type": "categorical",
            "count": count,
            "values": sorted_counts,
        }

    def _compute_grouped(
        self,
        reports: list[dict[str, str]],
        group_by: list[str],
        fields: list[str] | None = None,
    ) -> list[dict[str, Any]]:
        """Partition reports by group_by fields and compute per-group summaries."""
        groups: dict[tuple[tuple[str, str], ...], list[dict[str, str]]] = defaultdict(list)

        for report in reports:
            # Build group key from group_by fields
            key_parts: list[tuple[str, str]] = []
            for field in group_by:
                key_parts.append((field, report.get(field, "")))
            groups[tuple(key_parts)].append(report)

        result: list[dict[str, Any]] = []
        for key_tuple, group_reports in sorted(
            groups.items(), key=lambda x: len(x[1]), reverse=True
        ):
            # Exclude group_by fields from the field summary
            effective_fields = fields
            if effective_fields:
                effective_fields = [f for f in effective_fields if f not in group_by]

            group_entry: dict[str, Any] = {
                "key": dict(key_tuple),
                "count": len(group_reports),
                "field_summary": self._compute_field_summary(group_reports, effective_fields),
            }
            # Remove group_by fields from field_summary (they're in the key)
            for field in group_by:
                group_entry["field_summary"].pop(field, None)

            result.append(group_entry)

        return result
