"""HTTP client for calling remote sync API endpoints.

Features:
    - Gzip compression for push payloads (5-10x smaller)
    - Accept-Encoding: gzip for pull responses
    - SHA256 checksum for data integrity verification
    - Retry with exponential backoff
    - Paginated pull with streaming
"""
from __future__ import annotations

import gzip
import hashlib
import json
import logging
import time
from datetime import datetime
from typing import Generator

import requests

from app.services.data_sync.retry import RetryConfig, calculate_backoff

logger = logging.getLogger(__name__)


class RemoteSyncClient:
    """Calls the remote server's /api/v1/sync/* endpoints."""

    def __init__(self, remote_url: str, sync_key: str,
                 timeout: int = 120, batch_size: int = 1000):
        self.remote_url = remote_url.rstrip('/')
        self.sync_key = sync_key
        self.timeout = timeout
        self.batch_size = batch_size
        self.session = requests.Session()
        self.session.headers.update({
            'X-Sync-Key': sync_key,
            'Accept-Encoding': 'gzip, deflate',
        })
        self.retry_config = RetryConfig(
            max_retries=3,
            base_delay=5.0,
            max_delay=60.0,
            timeout=timeout,
        )

    # ── Pull (GET remote data) ───────────────────────────────────────

    def pull_table(self, table_name: str, since: datetime | None = None,
                   batch_size: int | None = None) -> Generator[list[dict], None, None]:
        """Pull records from remote in paginated batches.

        - Server responses are gzip-compressed (requests handles decompression)
        - SHA256 checksum verified on each page

        Yields:
            List of record dicts per page.
        """
        bs = batch_size or self.batch_size
        offset = 0
        total_pulled = 0

        while True:
            params = {'offset': offset, 'limit': bs}
            if since:
                params['since'] = since.isoformat()

            url = f'{self.remote_url}/api/v1/sync/pull/{table_name}'
            resp = self._get_with_retry(url, params, raw_response=True)

            if resp is None:
                logger.error('[sync-client] Pull %s failed at offset %d', table_name, offset)
                break

            # Verify integrity if server sent checksum
            body_bytes = resp.content
            server_hash = resp.headers.get('X-Content-SHA256')
            if server_hash:
                local_hash = hashlib.sha256(body_bytes).hexdigest()
                if local_hash != server_hash:
                    logger.error(
                        '[sync-client] Pull %s: SHA256 mismatch at offset %d! '
                        'Expected %s, got %s',
                        table_name, offset, server_hash[:16], local_hash[:16],
                    )
                    break

            try:
                data = resp.json()
            except Exception:
                logger.error('[sync-client] Pull %s: invalid JSON at offset %d', table_name, offset)
                break

            records = data.get('records', [])
            total_pulled += len(records)

            # Always yield the batch (even if empty) to signal remote was contacted
            # This lets the orchestrator distinguish "remote unreachable" (no yield)
            # from "remote returned 0 records" (yields empty list)
            yield records

            has_more = data.get('has_more', False)
            if not has_more or not records:
                break

            offset = data.get('next_offset', offset + bs)

        logger.info('[sync-client] Pull %s: %d records total', table_name, total_pulled)

    # ── Push (POST data to remote) ───────────────────────────────────

    def push_batch(self, table_name: str, records: list[dict]) -> dict | None:
        """Push a single batch of records with gzip compression + SHA256.

        Returns:
            Server response dict or None on failure.
        """
        url = f'{self.remote_url}/api/v1/sync/push/{table_name}'
        payload = {
            'records': records,
            'sync_timestamp': datetime.utcnow().isoformat(),
        }

        # Serialize to JSON bytes
        json_bytes = json.dumps(payload, ensure_ascii=False, separators=(',', ':')).encode('utf-8')

        # Calculate SHA256 of uncompressed JSON
        checksum = hashlib.sha256(json_bytes).hexdigest()

        # Gzip compress
        compressed = gzip.compress(json_bytes, compresslevel=6)
        ratio = len(json_bytes) / max(len(compressed), 1)

        logger.debug(
            '[sync-client] Push %s: %d records, %s → %s (%.1fx compression)',
            table_name, len(records),
            _fmt_bytes(len(json_bytes)), _fmt_bytes(len(compressed)), ratio,
        )

        headers = {
            'Content-Type': 'application/json',
            'Content-Encoding': 'gzip',
            'X-Content-SHA256': checksum,
            'X-Original-Size': str(len(json_bytes)),
        }

        result = self._post_with_retry(url, compressed, extra_headers=headers)
        if result is not None:
            result['_bytes_compressed'] = len(compressed)
            result['_bytes_uncompressed'] = len(json_bytes)
        return result

    def push_table(self, table_name: str, records: list[dict],
                   batch_size: int | None = None) -> dict:
        """Push records to remote in batches with compression.

        Returns:
            Summary dict with total upserted count and errors.
        """
        bs = batch_size or self.batch_size
        total = len(records)
        upserted = 0
        errors = []
        total_bytes_sent = 0

        for i in range(0, total, bs):
            batch = records[i:i + bs]
            batch_num = (i // bs) + 1
            total_batches = (total + bs - 1) // bs

            result = self.push_batch(table_name, batch)

            if result is None:
                err = f'Batch {batch_num}/{total_batches} failed'
                errors.append(err)
                logger.error('[sync-client] Push %s: %s', table_name, err)
            else:
                count = result.get('upserted', 0)
                upserted += count
                total_bytes_sent += result.get('_compressed_size', 0)
                logger.info(
                    '[sync-client] Push %s batch %d/%d: %d upserted',
                    table_name, batch_num, total_batches, count,
                )

        return {
            'table': table_name,
            'total_sent': total,
            'upserted': upserted,
            'errors': errors,
        }

    # ── Status ───────────────────────────────────────────────────────

    def check_status(self) -> dict | None:
        """Check remote server sync readiness."""
        url = f'{self.remote_url}/api/v1/sync/status'
        resp = self._get_with_retry(url, raw_response=True)
        if resp is not None:
            try:
                return resp.json()
            except Exception:
                return None
        return None

    # ── Internal HTTP helpers ────────────────────────────────────────

    def _get_with_retry(self, url: str, params: dict | None = None,
                        raw_response: bool = False):
        """GET with retry logic. Returns Response if raw_response else dict."""
        cfg = self.retry_config
        for attempt in range(cfg.max_retries + 1):
            try:
                resp = self.session.get(url, params=params, timeout=cfg.timeout)
                if resp.status_code < 400:
                    return resp if raw_response else resp.json()
                if resp.status_code == 401:
                    logger.error('[sync-client] Auth failed (401) — check SYNC_API_KEY')
                    return None
                if attempt < cfg.max_retries:
                    delay = calculate_backoff(attempt, cfg)
                    logger.warning(
                        '[sync-client] GET %s → HTTP %d, retry %d/%d in %.1fs',
                        url.split('/api/')[-1], resp.status_code,
                        attempt + 1, cfg.max_retries, delay,
                    )
                    time.sleep(delay)
                else:
                    logger.error('[sync-client] GET %s → HTTP %d, retries exhausted',
                                 url.split('/api/')[-1], resp.status_code)
                    return None
            except requests.exceptions.RequestException as e:
                if attempt < cfg.max_retries:
                    delay = calculate_backoff(attempt, cfg)
                    logger.warning('[sync-client] GET error: %s, retry in %.1fs',
                                   str(e)[:80], delay)
                    time.sleep(delay)
                else:
                    logger.error('[sync-client] GET error: %s, retries exhausted', str(e)[:80])
                    return None
        return None

    def _post_with_retry(self, url: str, body: bytes,
                         extra_headers: dict | None = None) -> dict | None:
        """POST with retry logic. Sends raw bytes (pre-compressed)."""
        cfg = self.retry_config
        headers = dict(extra_headers or {})

        for attempt in range(cfg.max_retries + 1):
            try:
                resp = self.session.post(
                    url, data=body, headers=headers, timeout=cfg.timeout,
                )
                if resp.status_code < 400:
                    result = resp.json()
                    result['_compressed_size'] = len(body)
                    return result
                if resp.status_code == 401:
                    logger.error('[sync-client] Auth failed (401) — check SYNC_API_KEY')
                    return None
                if resp.status_code == 422:
                    # Integrity check failed on server
                    try:
                        err_data = resp.json()
                        logger.error('[sync-client] Integrity error: %s', err_data.get('error'))
                    except Exception:
                        pass
                    return None
                if attempt < cfg.max_retries:
                    delay = calculate_backoff(attempt, cfg)
                    logger.warning(
                        '[sync-client] POST %s → HTTP %d, retry %d/%d in %.1fs',
                        url.split('/api/')[-1], resp.status_code,
                        attempt + 1, cfg.max_retries, delay,
                    )
                    time.sleep(delay)
                else:
                    logger.error('[sync-client] POST %s → HTTP %d, retries exhausted',
                                 url.split('/api/')[-1], resp.status_code)
                    return None
            except requests.exceptions.RequestException as e:
                if attempt < cfg.max_retries:
                    delay = calculate_backoff(attempt, cfg)
                    logger.warning('[sync-client] POST error: %s, retry in %.1fs',
                                   str(e)[:80], delay)
                    time.sleep(delay)
                else:
                    logger.error('[sync-client] POST error: %s, retries exhausted', str(e)[:80])
                    return None
        return None


def _fmt_bytes(n: int) -> str:
    """Human-readable byte size."""
    for unit in ('B', 'KB', 'MB', 'GB'):
        if abs(n) < 1024:
            return f'{n:.1f}{unit}'
        n /= 1024
    return f'{n:.1f}TB'
