"""uploader.py — gzip + HTTPS upload with retry/resume for trading events.

Architecture:
  Events → gzip compress → HTTPS POST /api/data/ingest
                                          │
                     ┌────────────────────┤
                     ▼                    ▼
                Success (200)       Retry (1s → 2s → 4s → 8s, max 60s)
                Update cursor        Write to retry queue
```

Features:
- gzip compression (~10x reduction for JSON text)
- Exponential backoff with jitter
- Cursor-based resume (never re-send events)
- Batch idempotency (batch_id prevents duplicates)
- TLS with certificate verification (opt-out for self-signed dev certs)
- Network check before attempting upload
"""

from __future__ import annotations

import base64
import gzip
import json
import os
import time
import uuid
import zlib
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import config_manager

from src.data_pipeline.anonymizer import hash_tenant_id

APP_DIR = config_manager.get_app_dir()
DATA_DIR = config_manager.get_data_dir()
LOGS_DIR = config_manager.get_logs_dir()

SERVER_BASE_URL = os.environ.get(
    "DATA_PIPELINE_SERVER_URL",
    os.environ.get("DATA_SERVER_URL", "https://43.167.210.121:8000"),
)
SERVER_INGEST_PATH = "/api/data/ingest"

# ── Retry queue ───────────────────────────────────────────────────
RETRY_QUEUE_FILE = DATA_DIR / "uploads" / "retry_queue.ndjson"


# ── Helpers ──────────────────────────────────────────────────────

def _load_json(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def _save_json(path: Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_name(f".{os.getpid()}.tmp")
    tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    tmp.replace(path)


def _append_ndjson(path: Path, record: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "a", encoding="utf-8") as f:
        f.write(json.dumps(record, ensure_ascii=False) + "\n")


def _gzip_compress(data: bytes | str) -> bytes:
    """Compress bytes or string with gzip. Returns raw compressed bytes."""
    if isinstance(data, str):
        data = data.encode("utf-8")
    return gzip.compress(data, compresslevel=6)


def _gzip_decompress(data: bytes) -> str:
    """Decompress gzip bytes back to string."""
    return gzip.decompress(data).decode("utf-8")


# ── Network check ──────────────────────────────────────────────────

def check_network(timeout: float = 3.0) -> bool:
    """Check if the server is reachable. Returns True if reachable."""
    try:
        import urllib.request
        url = f"{SERVER_BASE_URL}/api/data/health"
        req = urllib.request.Request(url, headers={"User-Agent": "GoldAutoTrade/1.0"})
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return resp.status == 200
    except Exception:
        return False


# ── Core upload ──────────────────────────────────────────────────

def upload_batch(
    events: list[dict[str, Any]],
    tenant_id: str,
    api_key: str | None = None,
    server_url: str | None = None,
    verify_tls: bool = True,
) -> dict[str, Any]:
    """Upload a batch of anonymized events to the server.

    Args:
        events: List of anonymized trade event dicts
        tenant_id: Raw tenant identifier (will be hashed)
        api_key: Server API key (X-Api-Key header). Falls back to env DATA_API_KEY.
        server_url: Override server base URL.
        verify_tls: Whether to verify TLS certificates.

    Returns:
        Result dict with keys: success (bool), batch_id, events_uploaded,
        server_response, error, retry_scheduled.
    """
    if not events:
        return {"success": True, "events_uploaded": 0, "batch_id": None, "error": None}

    base_url = server_url or SERVER_BASE_URL
    tenant_hash = hash_tenant_id(tenant_id)
    batch_id = str(uuid.uuid4())
    batch_ts = datetime.now(timezone.utc).isoformat()
    sequence = int(time.time())  # Approximate sequence number

    # ── Build payload ────────────────────────────────────────────
    events_json = json.dumps(events, ensure_ascii=False)
    compressed = _gzip_compress(events_json)
    payload_b64 = base64.b64encode(compressed).decode("ascii")

    payload = {
        "tenant_id_hash": tenant_hash,
        "batch_id": batch_id,
        "sequence": sequence,
        "batch_ts": batch_ts,
        "event_count": len(events),
        "compression": "gzip",
        "payload_b64": payload_b64,
    }

    # ── Resolve API key ──────────────────────────────────────────
    if not api_key:
        api_key = os.environ.get("DATA_API_KEY", "")

    # ── Build request ────────────────────────────────────────────
    url = f"{base_url}{SERVER_INGEST_PATH}"
    headers = {
        "Content-Type": "application/json",
        "X-Api-Key": api_key,
        "X-Tenant-Hash": tenant_hash,
        "X-Batch-Id": batch_id,
        "Content-Encoding": "gzip",
        "User-Agent": "GoldAutoTrade/DataPipeline/1.0",
    }

    body = json.dumps(payload).encode("utf-8")

    try:
        import urllib.request
        import urllib.error
        req = urllib.request.Request(url, data=body, headers=headers, method="POST")
        context = None
        if not verify_tls:
            import ssl
            context = ssl.create_default_context()
            context.check_hostname = False
            context.verify_mode = ssl.CERT_NONE

        with urllib.request.urlopen(req, timeout=30, context=context) as resp:
            data = resp.read()
            try:
                result = json.loads(data)
            except Exception:
                result = {"raw": data.decode("utf-8", errors="replace")}

        return {
            "success": True,
            "batch_id": batch_id,
            "events_uploaded": len(events),
            "server_response": result,
            "error": None,
            "retry_scheduled": False,
        }

    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8", errors="replace") if e.fp else ""
        error_msg = f"HTTP {e.code}: {body or e.reason}"
        # 409 Conflict = batch already processed (idempotent success)
        if e.code == 409:
            return {
                "success": True,
                "batch_id": batch_id,
                "events_uploaded": len(events),
                "server_response": {"status": "already_processed"},
                "error": None,
                "retry_scheduled": False,
            }
        # 4xx client error — don't retry, alert instead
        if 400 <= e.code < 500:
            return {
                "success": False,
                "batch_id": batch_id,
                "events_uploaded": 0,
                "server_response": {"status": "client_error", "code": e.code},
                "error": error_msg,
                "retry_scheduled": False,
            }
        # 5xx server error — schedule retry
        return _schedule_retry(batch_id, events, error_msg)

    except Exception as e:
        return _schedule_retry(batch_id, events, str(e))


def _schedule_retry(
    batch_id: str,
    events: list[dict[str, Any]],
    error: str,
) -> dict[str, Any]:
    """Write a failed batch to the retry queue for later processing."""
    record = {
        "batch_id": batch_id,
        "events": events,
        "error": error,
        "scheduled_at": datetime.now(timezone.utc).isoformat(),
        "retry_count": 0,
    }
    _append_ndjson(RETRY_QUEUE_FILE, record)
    return {
        "success": False,
        "batch_id": batch_id,
        "events_uploaded": 0,
        "server_response": None,
        "error": error,
        "retry_scheduled": True,
    }


# ── Retry queue processing ───────────────────────────────────────

def _load_retry_queue() -> list[dict[str, Any]]:
    if not RETRY_QUEUE_FILE.exists():
        return []
    try:
        records = []
        for line in RETRY_QUEUE_FILE.read_text(encoding="utf-8").strip().split("\n"):
            if line.strip():
                records.append(json.loads(line))
        return records
    except Exception:
        return []


def _save_retry_queue(records: list[dict[str, Any]]) -> None:
    if not records:
        # Truncate file
        if RETRY_QUEUE_FILE.exists():
            RETRY_QUEUE_FILE.write_text("", encoding="utf-8")
        return
    RETRY_QUEUE_FILE.write_text(
        "\n".join(json.dumps(r, ensure_ascii=False) for r in records) + "\n",
        encoding="utf-8",
    )


def process_retry_queue(
    tenant_id: str,
    api_key: str | None = None,
    max_retries: int = 5,
    max_wait_sec: float = 60.0,
) -> dict[str, Any]:
    """Process all pending retry records.

    Uses exponential backoff with jitter. Records that exceed
    max_retries are dropped and logged.

    Returns: {processed: N, succeeded: M, failed: K, dropped: D}
    """
    records = _load_retry_queue()
    if not records:
        return {"processed": 0, "succeeded": 0, "failed": 0, "dropped": 0}

    processed = succeeded = failed = dropped = 0
    _done_ids: set[str] = set()   # batches to remove (succeeded / exhausted)
    _drop_ids: set[str] = set()   # batches permanently dropped

    for record in records:
        batch_id = record.get("batch_id", "")
        events = record.get("events", [])
        retry_count = int(record.get("retry_count", 0) or 0)
        error = record.get("error", "")

        if retry_count >= max_retries:
            dropped += 1
            processed += 1
            _drop_ids.add(batch_id)
            print(f"[uploader] Dropped batch {batch_id} after {max_retries} retries: {error}")
            continue

        # Exponential backoff with jitter
        base_delay = min(2 ** retry_count, max_wait_sec)
        jitter = base_delay * 0.3 * (hash(batch_id) % 100 / 100)
        delay = base_delay + jitter

        # Check if enough time has passed since last attempt
        scheduled_at = record.get("scheduled_at", "")
        if scheduled_at:
            from src.data_pipeline.event_capture import _parse_ts
            dt = _parse_ts(scheduled_at)
            if dt:
                elapsed = (datetime.now(timezone.utc) - dt).total_seconds()
                if elapsed < delay:
                    # Not ready yet, skip for now
                    continue

        result = upload_batch(events, tenant_id, api_key=api_key)
        if result.get("success"):
            succeeded += 1
            processed += 1
            _done_ids.add(batch_id)
            print(f"[uploader] Retry succeeded for batch {batch_id}")
        else:
            # Update retry count and reschedule
            failed += 1
            processed += 1
            record["retry_count"] = retry_count + 1
            record["error"] = result.get("error", error)
            record["last_retry_at"] = datetime.now(timezone.utc).isoformat()
            record["scheduled_at"] = datetime.now(timezone.utc).isoformat()

    # Keep only records that haven't been resolved (neither succeeded nor dropped)
    remaining = [
        r for r in records
        if r.get("batch_id", "") not in _done_ids
        and r.get("batch_id", "") not in _drop_ids
        and r.get("retry_count", 0) < max_retries
    ]
    _save_retry_queue(remaining)

    return {
        "processed": processed,
        "succeeded": succeeded,
        "failed": failed,
        "dropped": dropped,
        "queue_depth": len(remaining),
    }
