"""API client for web backend integration (System 2 <-> System 1).

Allows the local executor to:
- Fetch simulation plans from the web backend
- Upload scan results back to the web backend

All source is ASCII.
"""

from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any

import urllib.request
import urllib.error


class WebAPIClient:
    """Client for the web backend REST API."""

    def __init__(self, base_url: str = "http://127.0.0.1:8000"):
        self.base_url = base_url.rstrip("/")
        self.timeout = 30

    def _request(self, method: str, path: str, data: dict | None = None,
                 files: dict | None = None) -> Any:
        """Make an HTTP request to the API."""
        url = f"{self.base_url}{path}"
        if files:
            # Multipart form upload
            import uuid
            boundary = uuid.uuid4().hex
            body = b""
            for field_name, file_info in files.items():
                body += f"--{boundary}\r\n".encode()
                body += f'Content-Disposition: form-data; name="{field_name}"; filename="{file_info["filename"]}"\r\n'.encode()
                body += b"Content-Type: application/octet-stream\r\n\r\n"
                body += file_info["content"]
                body += b"\r\n"
            body += f"--{boundary}--\r\n".encode()
            req = urllib.request.Request(url, data=body, method=method)
            req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
        else:
            body = json.dumps(data).encode() if data else None
            req = urllib.request.Request(url, data=body, method=method)
            req.add_header("Content-Type", "application/json")

        try:
            with urllib.request.urlopen(req, timeout=self.timeout) as resp:
                return json.loads(resp.read())
        except urllib.error.HTTPError as e:
            error_body = e.read().decode()
            raise RuntimeError(f"API {method} {path} failed: {e.code} {error_body}")
        except urllib.error.URLError as e:
            raise RuntimeError(f"API connection failed: {e.reason}")

    # -- Health --

    def health_check(self) -> bool:
        """Check if the web backend is reachable."""
        try:
            result = self._request("GET", "/api/health")
            return result.get("status") == "ok"
        except Exception:
            return False

    # -- Projects --

    def list_projects(self, topology: str | None = None) -> list[dict]:
        """List all projects."""
        path = "/api/projects"
        if topology:
            path += f"?topology={topology}"
        result = self._request("GET", path)
        return result.get("items", [])

    def get_project(self, project_id: int) -> dict:
        """Get a project by ID."""
        return self._request("GET", f"/api/projects/{project_id}")

    # -- Plans --

    def list_plans(self, project_id: int | None = None) -> list[dict]:
        """List plans, optionally filtered by project."""
        path = "/api/plans"
        if project_id:
            path += f"?project_id={project_id}"
        result = self._request("GET", path)
        return result.get("items", [])

    def get_plan(self, plan_id: int) -> dict:
        """Get a plan by ID."""
        return self._request("GET", f"/api/plans/{plan_id}")

    def download_plan(self, plan_id: int) -> dict:
        """Download a plan as simulation_plan.json (compatible with local executor)."""
        return self._request("GET", f"/api/plans/{plan_id}/download")

    def download_plan_by_uuid(self, plan_uuid: str) -> dict:
        """Download a plan by its plan_id string (e.g. SP-20260827-001)."""
        return self._request("GET", f"/api/plans/by-plan-id/{plan_uuid}/download")

    def upload_results(self, plan_id: int, csv_path: str | Path) -> dict:
        """Upload scan_results.csv to the web backend."""
        csv_path = Path(csv_path)
        if not csv_path.exists():
            raise FileNotFoundError(f"CSV file not found: {csv_path}")
        with open(csv_path, "rb") as f:
            content = f.read()
        return self._request(
            "POST",
            f"/api/plans/{plan_id}/upload-results",
            files={"file": {"filename": csv_path.name, "content": content}},
        )

    def get_plan_results(self, plan_id: int) -> list[dict]:
        """Get all results for a plan."""
        result = self._request("GET", f"/api/plans/{plan_id}/results")
        return result.get("items", [])

    # -- Experience Library --

    def list_experience(self, topology: str | None = None,
                         tag: str | None = None, limit: int = 50) -> list[dict]:
        """List experience cases with optional filters."""
        path = f"/api/experience?limit={limit}"
        if topology:
            path += f"&topology={topology}"
        if tag:
            path += f"&tag={tag}"
        result = self._request("GET", path)
        return result.get("items", [])

    def get_experience(self, case_id: int) -> dict:
        """Get an experience case by ID."""
        return self._request("GET", f"/api/experience/{case_id}")

    def create_experience(self, data: dict) -> dict:
        """Create an experience case.

        Args:
            data: dict with params, metrics, topology, conclusion, tags, rating, etc.
        """
        return self._request("POST", "/api/experience", data=data)

    def update_experience(self, case_id: int, data: dict) -> dict:
        """Update an experience case (conclusion, tags, rating, params, metrics)."""
        return self._request("PUT", f"/api/experience/{case_id}", data=data)

    def delete_experience(self, case_id: int) -> None:
        """Delete an experience case."""
        self._request("DELETE", f"/api/experience/{case_id}")

    def import_experience_from_plan(self, plan_id: int,
                                     tags: list[str] | None = None,
                                     rating: int = 0,
                                     auto_conclusion: bool = True) -> dict:
        """Import all OK results from a plan into the experience library.

        Returns dict with imported/skipped counts.
        """
        data = {
            "tags": tags or ["auto-imported"],
            "rating": rating,
            "auto_conclusion": auto_conclusion,
        }
        return self._request("POST", f"/api/experience/from-plan/{plan_id}", data=data)

    def import_experience_from_result(self, result_id: int,
                                       conclusion: str | None = None,
                                       tags: list[str] | None = None,
                                       rating: int = 0) -> dict:
        """Import a single simulation result into the experience library."""
        data = {
            "conclusion": conclusion or "",
            "tags": tags or ["auto-imported"],
            "rating": rating,
        }
        return self._request("POST", f"/api/experience/from-result/{result_id}", data=data)

    # -- Analytics --

    def get_metric_defs(self) -> list[dict]:
        """Get all available metric definitions."""
        result = self._request("GET", "/api/analytics/metrics")
        return result.get("metrics", [])

    def get_experience_stats(self, topology: str | None = None) -> dict:
        """Get experience library aggregate statistics."""
        path = "/api/analytics/experience/stats"
        if topology:
            path += f"?topology={topology}"
        return self._request("GET", path)

    def find_similar_experience(self, params: dict[str, float],
                                 topology: str | None = None,
                                 top_k: int = 5,
                                 tolerance: float = 0.3) -> list[dict]:
        """Find experience cases similar to target parameters."""
        query = f"?top_k={top_k}&tolerance={tolerance}"
        if topology:
            query += f"&topology={topology}"
        result = self._request("POST", f"/api/analytics/experience/similar{query}",
                                data={"params": params})
        return result.get("items", [])

    def get_plan_trend(self, plan_id: int, param_key: str, metric_key: str) -> dict:
        """Get parameter vs metric trend data for a plan."""
        return self._request("GET",
            f"/api/analytics/plans/{plan_id}/trend?param_key={param_key}&metric_key={metric_key}")

    def get_plan_pareto(self, plan_id: int, x_metric: str = "total_losses_w",
                        y_metric: str = "efficiency_pct") -> dict:
        """Get Pareto frontier data for a plan."""
        return self._request("GET",
            f"/api/analytics/plans/{plan_id}/pareto?x_metric={x_metric}&y_metric={y_metric}")

    def get_plan_sensitivity(self, plan_id: int, metric_key: str) -> dict:
        """Get parameter sensitivity ranking for a target metric."""
        return self._request("GET",
            f"/api/analytics/plans/{plan_id}/sensitivity?metric_key={metric_key}")

    def get_project_overview(self, project_id: int) -> dict:
        """Get project overview analytics (plans, results, best metrics)."""
        return self._request("GET", f"/api/analytics/projects/{project_id}/overview")

    # -- Sync: local experience DB to web --

    def sync_local_experience_to_web(self, local_db_path: str | Path,
                                      topology: str | None = None) -> dict:
        """Sync cases from local experience DB to the web experience library.

        Reads all runs from local SQLite DB and creates corresponding
        experience cases on the web backend. Skips duplicates by
        (plan_id + params signature).

        Returns dict with synced/skipped counts.
        """
        from .experience_db import ExperienceDB
        local_db = ExperienceDB(local_db_path)
        runs = local_db.get_all_runs(topology=topology, limit=1000)
        local_db.close()

        # Get existing web cases to avoid duplicates
        existing = self.list_experience(topology=topology, limit=500)
        existing_keys = set()
        for c in existing:
            key_parts = [c.get("source_plan_id", "")]
            for k in sorted(c.get("params", {}).keys()):
                key_parts.append(f"{k}={c['params'][k]}")
            existing_keys.add("|".join(key_parts))

        synced = 0
        skipped = 0
        for run in runs:
            key_parts = [run.get("plan_id", "")]
            for k in sorted(run.get("params", {}).keys()):
                key_parts.append(f"{k}={run['params'][k]}")
            key = "|".join(key_parts)
            if key in existing_keys:
                skipped += 1
                continue

            try:
                self.create_experience({
                    "source_plan_id": run.get("plan_id", ""),
                    "topology": run.get("topology", "SSSR"),
                    "model_path": run.get("model_path", ""),
                    "params": run.get("params", {}),
                    "metrics": run.get("metrics", {}),
                    "conclusion": run.get("notes", "") or "Synced from local experience DB",
                    "tags": ["local-sync"],
                    "rating": 0,
                })
                synced += 1
                existing_keys.add(key)
            except Exception:
                skipped += 1

        return {"synced": synced, "skipped": skipped, "total_local": len(runs)}

    # -- Convenience: full workflow --

    def fetch_and_save_plan(self, plan_id: int, output_dir: str | Path) -> Path:
        """Download a plan and save it as simulation_plan.json.

        Args:
            plan_id: Plan ID in the web backend.
            output_dir: Directory to save the plan JSON.

        Returns:
            Path to the saved simulation_plan.json.
        """
        plan_data = self.download_plan(plan_id)
        output_dir = Path(output_dir)
        output_dir.mkdir(parents=True, exist_ok=True)
        plan_uuid = plan_data.get("plan_id", "plan")
        output_path = output_dir / f"{plan_uuid}.json"
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(plan_data.get("plan_data", plan_data), f, indent=2, ensure_ascii=False)
        return output_path


def get_api_client() -> WebAPIClient:
    """Get API client with URL from environment variable or default."""
    base_url = os.environ.get("AFM_WEB_API_URL", "http://127.0.0.1:8000")
    return WebAPIClient(base_url)
