"""GBS (Global Broadband Statistics) tools for PT Research App.

MongoDB utils for Statistics and Sources. Used by PT Systems Agent for
human-supervised GBS data entry (see upc_query_agent issue #37).

Uses mongosh (shell) instead of PyMongo - avoids Python SSL issues in
cloud/EC2 environments. Requires mongosh installed on the host.

Ref: pt-research-app schema (Statistics, Sources)
"""

from typing import Optional

from point_topic_mcp.core.utils import check_env_vars
from point_topic_mcp.core import mongodb_utils as mongo
from dotenv import load_dotenv

load_dotenv()

# GBS tools require PT Research App MongoDB connection
if check_env_vars("gbs_tools", ["PT_RESEARCH_DATABASE_URI"]):

    # Valid enums from pt-research-app
    VALID_TYPES = ["broadband", "mobile", "iptv"]
    VALID_CHANNELS = ["Infrastructure", "Retail"]
    VALID_DOMAINS = ["Residential", "Business", "Total"]

    def _valid_object_id(oid: str) -> bool:
        """Check if string is valid 24-char hex ObjectId."""
        if not oid or len(oid) != 24:
            return False
        return all(c in "0123456789abcdefABCDEF" for c in oid)

    def list_operators(country: str = "", limit: int = 0, ctx=None) -> str:
        """
        List operators in the GBS database. Use before get_gbs_status to discover
        valid operator names and IDs.

        Args:
            country: Optional filter by country (exact match, e.g. "United Kingdom"). Leave empty for all.
            limit: Max operators to return. 0 = no limit. Useful to prevent context explosion.

        Returns:
            List of operators sorted by name, with name, country, operator_id (use in add_statistic/create_source).
        """
        ops = mongo.find_operators(country)
        
        if not ops:
            return f"No operators found" + (f" for country '{country}'" if country else "")
        
        # Sort by name (case-insensitive) for stable, predictable order
        ops_sorted = sorted(ops, key=lambda o: o.get("name", "").lower())
        
        # Warn on context explosion if no country filter and many operators
        if not country and len(ops_sorted) > 400:
            warning = f"⚠ WARNING: {len(ops_sorted)} operators total. Pass country filter or limit to prevent context overflow.\n\n"
        else:
            warning = ""
        
        # Build rows, optionally limited
        rows = []
        for op in ops_sorted[:limit] if limit > 0 else ops_sorted:
            oid = mongo._oid_from_doc(op)
            name = op.get("name", "?")
            c = op.get("country", "?")
            techs = op.get("technologies", [])
            rows.append(f"  {name} | {c} | id={oid} | techs={techs}")
        
        # Header showing filter applied
        filter_header = ""
        if country:
            filter_header = f"(country filter: '{country}', {len(ops_sorted)} match(es))\n"
        
        # Pagination footer if limited
        footer = ""
        if limit > 0 and len(ops_sorted) > limit:
            footer = f"\n... showing {limit} of {len(ops_sorted)} operators"
        
        return warning + filter_header + "Operators:\n" + "\n".join(rows) + footer

    def _quarter_distance(a: str, b: str) -> int | None:
        """Number of quarters between two period strings (e.g. '2025Q1' vs '2025Q3' → 2)."""
        try:
            ya, qa = int(a[:4]), int(a[-1])
            yb, qb = int(b[:4]), int(b[-1])
            return abs((ya * 4 + qa) - (yb * 4 + qb))
        except (ValueError, IndexError):
            return None

    def get_gbs_status(
        country: str,
        operator: str,
        period: str,
        ctx=None,
    ) -> str:
        """
        Get GBS status for a country/operator/period: what exists, gaps, staleness.

        Args:
            country: Country name (exact match, e.g. "United Kingdom" not "UK")
            operator: Operator name (exact match, case-sensitive)
            period: Quarter string (e.g. "2025Q1", "2024Q4")

        Returns:
            Summary of existing statistics, missing combinations (gaps), and
            staleness vs current stats period.
        """
        op = mongo.find_one_operator(operator, country)
        if not op:
            return f"Operator not found: '{operator}' in '{country}'. Check name and country match exactly."

        operator_id = mongo._oid_from_doc(op)
        technologies = op.get("technologies", [])

        gv = mongo.find_global_variables_current()
        current_period = gv.get("currentStatsPeriod", "unknown") if gv else "unknown"

        existing = mongo.find_statistics(operator_id, period)

        existing_combos = {
            (s["type"], s["tech"], s["channel"], s["domain"]) for s in existing
        }

        # Group gaps by type for readability
        gaps_by_type: dict[str, list[str]] = {}
        total_gaps = 0
        for t in VALID_TYPES:
            type_gaps = []
            for tech in technologies:
                for ch in VALID_CHANNELS:
                    for dom in VALID_DOMAINS:
                        if (t, tech, ch, dom) not in existing_combos:
                            type_gaps.append(f"    {tech} / {ch} / {dom}")
            if type_gaps:
                gaps_by_type[t] = type_gaps
                total_gaps += len(type_gaps)

        # State breakdown
        state_names = {1: "pending", 2: "approved", 3: "published"}
        by_state: dict[int, int] = {}
        by_type: dict[str, int] = {}
        for s in existing:
            st = s.get("state", 1)
            by_state[st] = by_state.get(st, 0) + 1
            tp = s.get("type", "unknown")
            by_type[tp] = by_type.get(tp, 0) + 1

        # Staleness
        dist = _quarter_distance(period, current_period)
        if period == current_period:
            staleness = "current"
        elif dist is not None:
            staleness = f"{dist} quarter{'s' if dist != 1 else ''} behind current ({current_period})"
        else:
            staleness = f"older than current ({current_period})"

        # Build output
        lines = [
            f"GBS status: {operator} ({country}) — {period}",
            f"operator_id: {operator_id}",
            f"technologies: {', '.join(technologies)}",
            "",
            f"Existing: {len(existing)} records",
        ]

        if by_type:
            lines.append(f"  by type: {', '.join(f'{k}: {v}' for k, v in sorted(by_type.items()))}")
        if by_state:
            lines.append(f"  by state: {', '.join(f'{state_names.get(k, k)}: {v}' for k, v in sorted(by_state.items()))}")

        lines.extend([
            "",
            f"Staleness: {staleness}",
            "",
            f"Gaps ({total_gaps} missing combinations):",
        ])

        for gtype, glist in gaps_by_type.items():
            lines.append(f"  {gtype} ({len(glist)}):")
            lines.extend(glist)

        if total_gaps == 0:
            lines.append("  None — all combinations covered")

        return "\n".join(lines)

    def _valid_period(p: str) -> bool:
        """Check period matches format like '2025Q1'."""
        import re
        return bool(re.match(r"^\d{4}Q[1-4]$", p))

    def add_statistic(
        type: str,
        operator_id: str,
        period: str,
        tech: str,
        channel: str,
        domain: str,
        subscribers: int,
        is_estimate: bool = False,
        restated: bool = False,
        notes: str = "",
        created_by: str = "gbs-agent",
        ctx=None,
    ) -> str:
        """
        Insert a Statistics (GBS) record with state: 1 (pending).

        Operator name and country are auto-filled from the operator record to prevent mismatches.

        Args:
            type: broadband | mobile | iptv
            operator_id: Operator ObjectId (from list_operators or get_gbs_status)
            period: Quarter string (e.g. "2025Q1")
            tech: Technology (must match operator's technologies)
            channel: Infrastructure | Retail
            domain: Residential | Business | Total
            subscribers: Subscriber count (must be >= 0)
            is_estimate: Whether value is estimated
            restated: Whether value was restated
            notes: Optional notes
            created_by: Creator identifier (e.g. user email)

        Returns:
            Inserted record summary or error message.
        """
        if type not in VALID_TYPES:
            return f"Invalid type: {type}. Must be one of {VALID_TYPES}"
        if channel not in VALID_CHANNELS:
            return f"Invalid channel: {channel}. Must be one of {VALID_CHANNELS}"
        if domain not in VALID_DOMAINS:
            return f"Invalid domain: {domain}. Must be one of {VALID_DOMAINS}"
        if not _valid_object_id(operator_id):
            return f"Invalid operator_id: must be 24 hex chars"
        if not _valid_period(period):
            return f"Invalid period: '{period}'. Must be format YYYYQN (e.g. '2025Q1')"
        if subscribers < 0:
            return f"Invalid subscribers: {subscribers}. Must be >= 0"

        op = mongo.find_one_operator_by_id(operator_id)
        if not op:
            return f"Operator not found: {operator_id}"

        op_name = op.get("name", "")
        op_country = op.get("country", "")
        techs = op.get("technologies", [])
        tech_match = next((t for t in techs if t.lower() == tech.lower()), None)
        if not tech_match:
            return f"Tech '{tech}' not valid for operator '{op_name}'. Valid: {techs}"

        # Check for duplicate (same operator, period, type, tech, channel, domain)
        existing = mongo.find_statistics(operator_id, period)
        for s in existing:
            if (s.get("type") == type and s.get("tech") == tech_match
                    and s.get("channel") == channel and s.get("domain") == domain):
                eid = mongo._oid_from_doc(s)
                return (f"Duplicate: statistic already exists for "
                        f"{type}/{tech_match}/{channel}/{domain} in {period} "
                        f"(id={eid}, state={s.get('state', '?')}). Not inserted.")

        doc = {
            "type": type,
            "country": op_country,
            "operator": op_name,
            "operatorId": operator_id,
            "period": period,
            "tech": tech_match,
            "channel": channel,
            "domain": domain,
            "subscribers": subscribers,
            "is_estimate": is_estimate,
            "restated": restated,
            "notes": notes or None,
            "createdBy": created_by,
        }
        inserted_id = mongo.insert_one_statistic(doc)
        return (f"Created statistic {inserted_id} (state: pending)\n"
                f"  {op_name} ({op_country}) | {period} | {type} | {tech_match} | "
                f"{channel} | {domain} | subscribers={subscribers}")

    def create_source(
        operator_id: str,
        year: int,
        quarter: int,
        type: str,
        url: str = "",
        file_url: Optional[str] = None,
        ctx=None,
    ) -> str:
        """
        Insert a Source record for an operator/period.

        Args:
            operator_id: Operator ObjectId (from list_operators or get_gbs_status)
            year: Year (e.g. 2025)
            quarter: Quarter 1-4
            type: Source type (e.g. "report", "earnings_call", "tariff", "regulatory_filing")
            url: Source URL (web link to the source)
            file_url: Optional file URL (e.g. S3 path for uploaded documents)

        Returns:
            Inserted record summary or error message.
        """
        if not _valid_object_id(operator_id):
            return f"Invalid operator_id: must be 24 hex chars"
        if quarter < 1 or quarter > 4:
            return f"Invalid quarter: {quarter}. Must be 1-4"
        if year < 1990 or year > 2100:
            return f"Invalid year: {year}. Must be 1990-2100"
        if not type or not type.strip():
            return "Invalid type: must not be empty"
        if not url and not file_url:
            return "At least one of url or file_url must be provided"

        op = mongo.find_one_operator_by_id(operator_id)
        if not op:
            return f"Operator not found: {operator_id}"

        op_name = op.get("name", "")

        # Duplicate check: same operator/year/quarter/type/url
        existing = mongo.find_sources(operator_id, year, quarter)
        for s in existing:
            if (s.get("type") == type
                    and s.get("url") == (url or None)
                    and s.get("fileUrl") == file_url):
                eid = mongo._oid_from_doc(s)
                return f"Duplicate: source already exists (id={eid}). Not inserted."

        doc = {
            "operatorId": operator_id,
            "year": year,
            "quarter": quarter,
            "type": type,
            "url": url or None,
            "fileUrl": file_url,
        }
        inserted_id = mongo.insert_one_source(doc)
        return (f"Created source {inserted_id}\n"
                f"  {op_name} | {year}Q{quarter} | type={type} | url={url or file_url or 'none'}")


def _run_test_flow():
    """Run GBS tools with real MongoDB. Use: uv run python -m point_topic_mcp.tools.gbs_tools"""
    import re
    import sys

    mod_name = "point_topic_mcp.tools.gbs_tools"
    gbs = sys.modules.get(mod_name) or sys.modules.get("__main__")
    if not gbs or not hasattr(gbs, "list_operators"):
        print("SKIP: GBS tools not loaded (PT_RESEARCH_DATABASE_URI not set)")
        return

    print("=== GBS Tools Test Flow ===\n")

    print("1. list_operators(country='United Kingdom', limit=5):")
    try:
        out = gbs.list_operators(country="United Kingdom", limit=5)
        print(out)
    except Exception as e:
        print(f"   FAIL: {e}")
        return

    # Extract first operator name and id from output
    op_id = None
    op_name = ""
    country = "United Kingdom"
    for line in out.split("\n"):
        if "|" in line and "id=" in line:
            parts = line.split("|")
            op_name = parts[0].strip()
            m = re.search(r"id=([a-f0-9]{24})", line)
            if m:
                op_id = m.group(1)
            break

    if not op_id:
        print("   No UK operators found - cannot continue test")
        return

    # Get the operator's first tech for a valid insert
    op_tech = ""
    for line in out.split("\n"):
        if op_id in line and "techs=[" in line:
            m = re.search(r"techs=\['([^']+)'", line)
            if m:
                op_tech = m.group(1)
            break

    print(f"\n2. get_gbs_status('{country}', '{op_name}', '2025Q1'):")
    try:
        status = gbs.get_gbs_status(country, op_name, "2025Q1")
        print(status[:800] + ("..." if len(status) > 800 else ""))
    except Exception as e:
        print(f"   FAIL: {e}")

    print("\n3. add_statistic() validation:")
    try:
        result = gbs.add_statistic(
            type="invalid",
            operator_id="000000000000000000000001",
            period="2025Q1",
            tech="FTTP",
            channel="Wrong",
            domain="Residential",
            subscribers=1000,
        )
        assert "Invalid" in result
        print("   OK: rejected invalid type")
    except Exception as e:
        print(f"   FAIL: {e}")

    print(f"\n4. add_statistic() real insert for {op_name} (tech={op_tech}):")
    try:
        result = gbs.add_statistic(
            type="broadband",
            operator_id=op_id,
            period="2099Q1",
            tech=op_tech,
            channel="Infrastructure",
            domain="Residential",
            subscribers=99999,
            notes="__gbs_tools_test_flow__",
            created_by="gbs-test-script",
        )
        print(f"   {result}")
    except Exception as e:
        print(f"   FAIL: {e}")

    print(f"\n5. create_source() real insert for {op_name}:")
    try:
        result = gbs.create_source(
            operator_id=op_id,
            year=2099,
            quarter=1,
            type="test",
            url="https://example.com/gbs-test",
        )
        print(f"   {result}")
    except Exception as e:
        print(f"   FAIL: {e}")

    print("\n=== Test flow complete ===")
    print("NOTE: test records inserted with period 2099Q1 / year 2099 — delete from dev when done")


if __name__ == "__main__":
    _run_test_flow()
