"""HTTP syncer — communicates with the RubberDuck MCP server.

Handles the manifest comparison + file upload protocol:
1. POST /index/manifest — send hashes, get back list of files to upload
2. POST /index/upload — send changed files (gzip compressed)
3. GET /index/status — check project index state
"""

import io
import json
import logging
import tarfile
from pathlib import Path
from typing import Any, Dict, List, Optional

import requests

logger = logging.getLogger(__name__)


class SyncError(Exception):
    """Raised when sync fails."""
    pass


class Syncer:
    """HTTP client for the RubberDuck indexing endpoints."""

    def __init__(self, server_url: str, token: str = "", timeout: int = 60):
        self.server_url = server_url.rstrip("/")
        self.timeout = timeout
        self.session = requests.Session()
        if token:
            self.session.headers["Authorization"] = f"Bearer {token}"

    def compare_manifest(
        self, project: str, file_hashes: Dict[str, str]
    ) -> Dict[str, Any]:
        """Send file hashes to server, get back what needs uploading.

        Args:
            project: Project name.
            file_hashes: {relative_path: sha256_hex}

        Returns:
            Server response with need_upload, already_synced, deleted_on_client.
        """
        url = f"{self.server_url}/index/manifest"
        payload = {"project": project, "files": file_hashes}

        try:
            resp = self.session.post(url, json=payload, timeout=self.timeout)
            resp.raise_for_status()
            return resp.json()
        except requests.RequestException as e:
            raise SyncError(f"Manifest comparison failed: {e}") from e

    def upload_files(
        self,
        project: str,
        files: Dict[str, bytes],
        deleted: Optional[List[str]] = None,
    ) -> Dict[str, Any]:
        """Upload changed files to the server.

        For small batches (<10 files, <1MB total), uses JSON mode.
        For larger batches, uses gzip tar mode.

        Args:
            project: Project name.
            files: {relative_path: file_bytes}
            deleted: List of paths that were deleted locally.

        Returns:
            Server response with sync results + graphs rebuilt.
        """
        if not files and not deleted:
            return {"synced": 0, "message": "Nothing to upload"}

        total_size = sum(len(v) for v in files.values())

        # Small batch: JSON mode (simpler)
        if len(files) < 10 and total_size < 1_000_000:
            return self._upload_json(project, files, deleted)

        # Large batch: gzip tar mode (more efficient)
        return self._upload_gzip(project, files, deleted)

    def _upload_json(
        self,
        project: str,
        files: Dict[str, bytes],
        deleted: Optional[List[str]],
    ) -> Dict[str, Any]:
        """Upload via JSON (for small batches)."""
        url = f"{self.server_url}/index/upload"

        # Convert bytes to strings for JSON
        files_str = {}
        for path, content in files.items():
            try:
                files_str[path] = content.decode("utf-8")
            except UnicodeDecodeError:
                logger.warning(f"Skipping binary file: {path}")
                continue

        payload: Dict[str, Any] = {"project": project, "files": files_str}
        if deleted:
            payload["deleted"] = deleted

        try:
            resp = self.session.post(
                url, json=payload, timeout=self.timeout
            )
            resp.raise_for_status()
            return resp.json()
        except requests.RequestException as e:
            raise SyncError(f"JSON upload failed: {e}") from e

    def _upload_gzip(
        self,
        project: str,
        files: Dict[str, bytes],
        deleted: Optional[List[str]],
    ) -> Dict[str, Any]:
        """Upload via gzip-compressed tar archive (for large batches)."""
        url = f"{self.server_url}/index/upload"

        # Build tar archive in memory
        buf = io.BytesIO()
        with tarfile.open(fileobj=buf, mode="w:gz") as tar:
            for rel_path, content in files.items():
                info = tarfile.TarInfo(name=rel_path)
                info.size = len(content)
                tar.addfile(info, io.BytesIO(content))

        archive_data = buf.getvalue()
        logger.info(
            f"Uploading {len(files)} files ({len(archive_data):,} bytes compressed)"
        )

        try:
            resp = self.session.post(
                url,
                data=archive_data,
                headers={
                    "Content-Type": "application/gzip",
                    "X-Project": project,
                },
                timeout=self.timeout,
            )
            resp.raise_for_status()
            result = resp.json()

            # Handle deletions separately if needed
            if deleted:
                del_resp = self.session.post(
                    f"{self.server_url}/index/remove",
                    json={"project": project, "delete_files": deleted},
                    timeout=self.timeout,
                )
                if del_resp.ok:
                    result["deleted"] = len(deleted)

            return result
        except requests.RequestException as e:
            raise SyncError(f"Gzip upload failed: {e}") from e

    def get_status(self, project: Optional[str] = None) -> Dict[str, Any]:
        """Get index status for a project or list all projects.

        Args:
            project: Project name. If None, lists all projects.
        """
        url = f"{self.server_url}/index/status"
        params = {}
        if project:
            params["project"] = project

        try:
            resp = self.session.get(url, params=params, timeout=self.timeout)
            resp.raise_for_status()
            return resp.json()
        except requests.RequestException as e:
            raise SyncError(f"Status check failed: {e}") from e

    def remove_project(self, project: str) -> Dict[str, Any]:
        """Remove an indexed project from the server."""
        url = f"{self.server_url}/index/remove"
        try:
            resp = self.session.post(
                url, json={"project": project}, timeout=self.timeout
            )
            resp.raise_for_status()
            return resp.json()
        except requests.RequestException as e:
            raise SyncError(f"Project removal failed: {e}") from e

    def list_projects(self) -> Dict[str, Any]:
        """List all indexed projects on the server."""
        return self.get_status()
