"""HTTP client for Nogic backend API."""

import hashlib
import json
import os
from dataclasses import dataclass
from typing import Iterator, Optional

import httpx

from nogic.config import Config


def get_directory_hash(directory: str = None) -> str:
    """Generate SHA256 hash of absolute directory path."""
    if directory is None:
        directory = os.getcwd()
    abs_path = os.path.abspath(directory)
    return hashlib.sha256(abs_path.encode()).hexdigest()


@dataclass
class IndexProgress:
    stage: str
    message: str
    current: int
    total: int
    files_indexed: Optional[int] = None
    files_skipped: Optional[int] = None
    nodes_created: Optional[int] = None
    edges_created: Optional[int] = None
    errors: Optional[list[dict]] = None


@dataclass
class IndexResult:
    status: str
    files_indexed: int
    files_skipped: int
    nodes_created: int
    edges_created: int
    errors: list[dict]


@dataclass
class UploadResult:
    """Result from /v1/index/upload endpoint."""
    files_parsed: int
    total_staged: int
    functions_found: int
    classes_found: int


@dataclass
class StagingStats:
    """Stats from /v1/index/staging/{project_id} endpoint."""
    files: int
    functions: int
    classes: int
    methods: int


@dataclass
class GraphWipeResult:
    message: str
    nodes_deleted: int


@dataclass
class Project:
    id: str
    name: str
    directory_hash: Optional[str] = None
    owner_id: Optional[str] = None
    created_at: Optional[str] = None
    updated_at: Optional[str] = None


class NogicClient:
    def __init__(self, config: Config):
        self.config = config
        self.base_url = config.api_url.rstrip("/")
        self._client: Optional[httpx.Client] = None

    @property
    def client(self) -> httpx.Client:
        if self._client is None:
            self._client = httpx.Client(
                base_url=self.base_url,
                timeout=60.0,
                headers=self._headers(),
            )
        return self._client

    def _headers(self) -> dict:
        headers = {"Content-Type": "application/json"}
        if self.config.api_key:
            headers["Authorization"] = f"Bearer {self.config.api_key}"
        return headers

    def close(self):
        if self._client:
            self._client.close()
            self._client = None

    def verify_key(self) -> dict:
        """Verify API key. Returns user info or raises."""
        headers = {}
        if self.config.api_key:
            headers["X-API-Key"] = self.config.api_key
        resp = self.client.post("/v1/keys/verify", headers=headers)
        resp.raise_for_status()
        return resp.json()

    def list_projects(self) -> list[Project]:
        """List user's projects."""
        resp = self.client.get("/v1/projects")
        resp.raise_for_status()
        data = resp.json()
        return [
            Project(
                id=p["id"],
                name=p["name"],
                directory_hash=p.get("directory_hash"),
                created_at=p.get("created_at"),
                updated_at=p.get("updated_at"),
            )
            for p in data["projects"]
        ]

    def get_project_by_directory(self, directory_hash: str) -> Optional[Project]:
        """
        Lookup project by directory hash.
        Returns None if no project found (404).
        """
        resp = self.client.get(
            "/v1/projects/by-directory",
            params={"hash": directory_hash},
        )
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        data = resp.json()
        return Project(
            id=data["id"],
            name=data["name"],
            directory_hash=data.get("directory_hash"),
            owner_id=data.get("owner_id"),
            created_at=data.get("created_at"),
            updated_at=data.get("updated_at"),
        )

    def create_project(
        self, name: str, directory_hash: Optional[str] = None
    ) -> Project:
        """
        Create a new project.

        Args:
            name: Project name
            directory_hash: SHA256 hash of absolute directory path

        Returns:
            Project object

        Raises:
            httpx.HTTPStatusError: 409 if project already exists for directory
        """
        payload = {"name": name}
        if directory_hash:
            payload["directory_hash"] = directory_hash

        resp = self.client.post("/v1/projects", json=payload)
        resp.raise_for_status()
        data = resp.json()
        return Project(
            id=data.get("project_id") or data.get("id"),
            name=data["name"],
            directory_hash=data.get("directory_hash"),
        )

    def wipe_project_graph(self, project_id: str) -> GraphWipeResult:
        """
        Wipe all graph data for a project (for reindexing).

        Deletes all nodes/edges in Neo4j but keeps project record in PostgreSQL.
        Uses a longer timeout since batched deletion of large graphs can take time.
        """
        resp = self.client.delete(
            f"/v1/projects/{project_id}/graph",
            timeout=300.0,
        )
        resp.raise_for_status()
        data = resp.json()
        return GraphWipeResult(
            message=data["message"],
            nodes_deleted=data["nodes_deleted"],
        )

    def wipe_project_graph_stream(self, project_id: str) -> Iterator[IndexProgress]:
        """
        Wipe graph data with SSE streaming progress.

        Yields IndexProgress events with stage "deleting" (current/total)
        and "complete" when done.
        Falls back to non-streaming wipe if server returns 404.
        """
        url = f"{self.base_url}/v1/projects/{project_id}/graph/stream"

        with httpx.stream(
            "DELETE",
            url,
            headers={**self._headers(), "Accept": "text/event-stream"},
            timeout=300.0,
        ) as response:
            if response.status_code == 404:
                # Server doesn't support streaming wipe, fall back
                result = self.wipe_project_graph(project_id)
                yield IndexProgress(
                    stage="complete",
                    message=result.message,
                    current=result.nodes_deleted,
                    total=result.nodes_deleted,
                )
                return
            response.raise_for_status()
            for line in response.iter_lines():
                if line.startswith("data: "):
                    data = json.loads(line[6:].strip())
                    yield IndexProgress(
                        stage=data["stage"],
                        message=data["message"],
                        current=data["current"],
                        total=data["total"],
                    )

    def delete_files(self, project_id: str, paths: list[str]) -> int:
        """Delete file nodes from the graph. Returns number of nodes deleted."""
        resp = self.client.post(
            "/v1/index/delete-files",
            json={"project_id": project_id, "paths": paths},
        )
        resp.raise_for_status()
        return resp.json().get("deleted_nodes", 0)

    def index_files(self, project_id: str, files: list[dict]) -> IndexResult:
        """
        Index raw code files (synchronous endpoint).

        files: [{"path": str, "content": str, "language": str}]
        """
        resp = self.client.post(
            "/v1/index",
            json={"project_id": project_id, "files": files},
        )
        resp.raise_for_status()
        data = resp.json()
        return IndexResult(
            status=data["status"],
            files_indexed=data["files_indexed"],
            files_skipped=data.get("files_skipped", 0),
            nodes_created=data["nodes_created"],
            edges_created=data["edges_created"],
            errors=data.get("errors", []),
        )

    def index_files_stream(
        self, project_id: str, files: list[dict]
    ) -> Iterator[IndexProgress]:
        """
        Index files with SSE streaming progress updates.

        files: [{"path": str, "content": str, "language": str}]
        Yields IndexProgress events as they arrive.
        """
        url = f"{self.base_url}/v1/index/stream"
        payload = {"project_id": project_id, "files": files}

        with httpx.stream(
            "POST",
            url,
            json=payload,
            headers={**self._headers(), "Accept": "text/event-stream"},
            timeout=300.0,
        ) as response:
            response.raise_for_status()
            for line in response.iter_lines():
                if line.startswith("data: "):
                    data = json.loads(line[6:])
                    yield IndexProgress(
                        stage=data["stage"],
                        message=data["message"],
                        current=data["current"],
                        total=data["total"],
                        files_indexed=data.get("files_indexed"),
                        files_skipped=data.get("files_skipped"),
                        nodes_created=data.get("nodes_created"),
                        edges_created=data.get("edges_created"),
                        errors=data.get("errors"),
                    )

    # -------------------------------------------------------------------------
    # Two-phase indexing endpoints (parallel upload + finalize)
    # -------------------------------------------------------------------------

    def upload_batch(self, project_id: str, files: list[dict]) -> UploadResult:
        """
        Upload and parse files without resolution (Phase 1).

        Stages files for later finalization. Fast (~2-3s per batch).
        files: [{"path": str, "content": str, "language": str, "hash": str}]
        """
        resp = self.client.post(
            "/v1/index/upload",
            json={"project_id": project_id, "files": files},
        )
        resp.raise_for_status()
        data = resp.json()
        return UploadResult(
            files_parsed=data["files_parsed"],
            total_staged=data["total_staged"],
            functions_found=data["functions_found"],
            classes_found=data["classes_found"],
        )

    def finalize_stream(self, project_id: str) -> Iterator[IndexProgress]:
        """
        Finalize staged files with global resolution (Phase 2).

        Performs: imports resolution, call graph building, edge creation, embeddings.
        Returns SSE stream with progress updates.
        """
        url = f"{self.base_url}/v1/index/finalize/stream"
        payload = {"project_id": project_id}

        with httpx.stream(
            "POST",
            url,
            json=payload,
            headers={**self._headers(), "Accept": "text/event-stream"},
            timeout=600.0,  # Finalization can take longer for large projects
        ) as response:
            response.raise_for_status()
            for line in response.iter_lines():
                if line.startswith("data: "):
                    data = json.loads(line[6:])
                    yield IndexProgress(
                        stage=data["stage"],
                        message=data["message"],
                        current=data["current"],
                        total=data["total"],
                        files_indexed=data.get("files_indexed"),
                        files_skipped=data.get("files_skipped"),
                        nodes_created=data.get("nodes_created"),
                        edges_created=data.get("edges_created"),
                        errors=data.get("errors"),
                    )

    def get_staging_stats(self, project_id: str) -> Optional[StagingStats]:
        """
        Get staging stats for a project.

        Returns None if server doesn't support two-phase indexing (404 or unexpected response).
        """
        resp = self.client.get(f"/v1/index/staging/{project_id}")
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        data = resp.json()
        # Check for expected response format
        if "files" not in data:
            return None
        return StagingStats(
            files=data["files"],
            functions=data["functions"],
            classes=data["classes"],
            methods=data["methods"],
        )

    def clear_staging(self, project_id: str) -> bool:
        """
        Clear staged files for a project (cancel/cleanup).

        Returns True if cleared, False if nothing to clear.
        """
        resp = self.client.delete(f"/v1/index/staging/{project_id}")
        if resp.status_code == 404:
            return False
        resp.raise_for_status()
        return True
