"""MongoDB utils using mongosh (shell) for robust connectivity.

Uses mongosh subprocess instead of PyMongo - avoids Python SSL issues that
fail in some cloud/EC2 environments. mongosh has its own SSL stack and
tlsAllowInvalidCertificates works reliably.

Requires: mongosh installed on the system.
  - Ubuntu/Debian: apt install mongodb-mongosh
  - Amazon Linux/RHEL: yum install mongodb-mongosh
  - macOS: brew install mongosh
"""

import json
import os
import subprocess
from typing import Any

from dotenv import load_dotenv

load_dotenv()


# Collections (pt-research-app Prisma schema)
OPERATOR_COLLECTION = "Operator"
STATISTICS_COLLECTION = "Statistics"
SOURCES_COLLECTION = "Sources"
GLOBAL_VARIABLES_COLLECTION = "GlobalVariables"


def _js_str(s: str | None) -> str:
    """Escape string for safe use in mongosh JS. Returns JSON-formatted string or null."""
    if s is None:
        return "null"
    return json.dumps(str(s))


def _uri_with_tls_option() -> str:
    uri = os.getenv("PT_RESEARCH_DATABASE_URI")
    if not uri:
        raise RuntimeError("PT_RESEARCH_DATABASE_URI is not set")
    if os.getenv("PT_RESEARCH_TLS_SKIP_VERIFY", "").strip() in ("1", "true", "yes"):
        sep = "&" if "?" in uri else "?"
        uri = f"{uri}{sep}tlsAllowInvalidCertificates=true"
    return uri


def _run_mongosh(eval_js: str, timeout: int = 15) -> str:
    """Run mongosh --eval and return stdout. Raises on failure."""
    uri = _uri_with_tls_option()
    try:
        result = subprocess.run(
            ["mongosh", uri, "--eval", eval_js, "--quiet"],
            capture_output=True,
            text=True,
            timeout=timeout,
        )
    except FileNotFoundError:
        raise RuntimeError(
            "mongosh not found. GBS tools require mongosh. "
            "Install with: point-topic-mcp-install-mongosh  (or sudo point-topic-mcp-install-mongosh on Linux)"
        ) from None
    if result.returncode != 0:
        raise RuntimeError(
            f"mongosh failed: {result.stderr or result.stdout or result.returncode}"
        )
    return result.stdout.strip()


def _oid_from_doc(doc: dict) -> str:
    """Extract ObjectId string from EJSON doc (handles {_id: {$oid: "..."}})."""
    if not doc:
        return ""
    id_val = doc.get("_id")
    if isinstance(id_val, dict) and "$oid" in id_val:
        return id_val["$oid"]
    if isinstance(id_val, str):
        return id_val
    return str(id_val)


def find_operators(country: str = "") -> list[dict[str, Any]]:
    """List operators. country="" for all."""
    query = f'{{country: {_js_str(country)}}}' if country else "{}"
    # Use EJSON.stringify for proper ObjectId handling
    js = f"EJSON.stringify(db.{OPERATOR_COLLECTION}.find({query}).toArray())"
    out = _run_mongosh(js)
    data = json.loads(out) if out else []
    return data if isinstance(data, list) else [data]


def find_one_operator(name: str, country: str) -> dict | None:
    """Find operator by name and country."""
    query = f'{{name: {_js_str(name)}, country: {_js_str(country)}}}'
    js = f"EJSON.stringify(db.{OPERATOR_COLLECTION}.findOne({query}))"
    out = _run_mongosh(js)
    data = json.loads(out) if out else None
    return data if data and data.get("_id") else None


def find_one_operator_by_id(oid_str: str) -> dict | None:
    """Find operator by ObjectId string."""
    # mongosh: ObjectId("hex")
    query = f'ObjectId("{oid_str}")'
    js = f"EJSON.stringify(db.{OPERATOR_COLLECTION}.findOne({{_id: {query}}}))"
    out = _run_mongosh(js)
    data = json.loads(out) if out else None
    return data if data and data.get("_id") else None


def find_statistics(operator_id_oid: str, period: str) -> list[dict]:
    """Find statistics for operator and period."""
    period_js = _js_str(period)
    js = f"""EJSON.stringify(
      db.{STATISTICS_COLLECTION}.find({{
        operatorId: ObjectId("{operator_id_oid}"),
        period: {period_js}
      }}).toArray()
    )"""
    out = _run_mongosh(js)
    data = json.loads(out) if out else []
    return data if isinstance(data, list) else [data]


def find_global_variables_current() -> dict | None:
    """Get current GlobalVariables record."""
    js = f"EJSON.stringify(db.{GLOBAL_VARIABLES_COLLECTION}.findOne({{isCurrent: true}}))"
    out = _run_mongosh(js)
    data = json.loads(out) if out else None
    return data if data else None


def insert_one_statistic(doc: dict) -> str:
    """Insert one Statistics document. Returns inserted ObjectId string."""
    oid = doc["operatorId"]  # ObjectId string
    op = doc["operator"].replace("\\", "\\\\").replace('"', '\\"')
    tech = doc["tech"].replace("\\", "\\\\").replace('"', '\\"')
    notes = (doc.get("notes") or "").replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
    created_by = (doc.get("createdBy") or "gbs-agent").replace("\\", "\\\\").replace('"', '\\"')
    # Prisma @default / @updatedAt do not apply to raw inserts — set BSON Date explicitly
    # so the web app receives real Date fields (same instant for date / createdAt / updatedAt).
    js = f"""
    const now = new Date();
    const result = db.{STATISTICS_COLLECTION}.insertOne({{
      type: "{doc["type"]}",
      country: "{doc["country"]}",
      operator: "{op}",
      operatorId: ObjectId("{oid}"),
      period: "{doc["period"]}",
      channel: "{doc["channel"]}",
      domain: "{doc["domain"]}",
      state: 1,
      tech: "{tech}",
      subscribers: {doc["subscribers"]},
      is_estimate: {str(doc.get("is_estimate", False)).lower()},
      restated: {str(doc.get("restated", False)).lower()},
      notes: {f'"{notes}"' if doc.get("notes") else "null"},
      is_archived: false,
      createdBy: "{created_by}",
      updatedBy: "{created_by}",
      date: now,
      createdAt: now,
      updatedAt: now
    }});
    EJSON.stringify(result.insertedId);
    """
    out = _run_mongosh(js)
    # Result is {"$oid":"..."} - extract the oid
    data = json.loads(out) if out else {}
    if isinstance(data, dict) and "$oid" in data:
        return data["$oid"]
    return str(data)


def insert_one_source(doc: dict) -> str:
    """Insert one Source document. Returns inserted ObjectId string."""
    oid = doc["operatorId"]
    url = (doc.get("url") or "").replace("\\", "\\\\").replace('"', '\\"')
    file_url = (doc.get("fileUrl") or "").replace("\\", "\\\\").replace('"', '\\"')
    type_val = doc["type"].replace("\\", "\\\\").replace('"', '\\"')
    # Sources requires createdAt + updatedAt (Prisma); raw inserts must set BSON Date.
    js = f"""
    const now = new Date();
    const result = db.{SOURCES_COLLECTION}.insertOne({{
      operatorId: ObjectId("{oid}"),
      year: {doc["year"]},
      quarter: {doc["quarter"]},
      type: "{type_val}",
      url: {f'"{url}"' if doc.get("url") else "null"},
      fileUrl: {f'"{file_url}"' if doc.get("fileUrl") else "null"},
      createdAt: now,
      updatedAt: now
    }});
    EJSON.stringify(result.insertedId);
    """
    out = _run_mongosh(js)
    data = json.loads(out) if out else {}
    if isinstance(data, dict) and "$oid" in data:
        return data["$oid"]
    return str(data)


def find_sources(operator_id_oid: str, year: int, quarter: int) -> list[dict]:
    """Find sources for operator, year, and quarter."""
    js = f"""EJSON.stringify(
      db.{SOURCES_COLLECTION}.find({{
        operatorId: ObjectId("{operator_id_oid}"),
        year: {year},
        quarter: {quarter}
      }}).toArray()
    )"""
    out = _run_mongosh(js)
    data = json.loads(out) if out else []
    return data if isinstance(data, list) else [data]


def mongosh_available() -> bool:
    """Check if mongosh is installed and can connect."""
    try:
        _run_mongosh("db.runCommand({ping:1})")
        return True
    except Exception:
        return False
