"""MLNGI HTTP Client - 백엔드 API와 통신하는 저수준 클라이언트"""

import requests
from typing import Any


class MLNGIClient:
    """MLNGI 서버와 통신하는 HTTP 클라이언트."""

    def __init__(self, tracking_uri: str, api_key: str | None = None, token: str | None = None):
        self.tracking_uri = tracking_uri.rstrip("/")
        self.api_key = api_key
        self.token = token
        self._headers = {"Content-Type": "application/json"}
        if api_key:
            self._headers["X-API-Key"] = api_key
        elif token:
            self._headers["Authorization"] = f"Bearer {token}"

    def _url(self, path: str) -> str:
        return f"{self.tracking_uri}{path}"

    def _request(self, method: str, path: str, **kwargs) -> Any:
        resp = requests.request(method, self._url(path), headers=self._headers, **kwargs)
        if resp.status_code == 401:
            raise PermissionError("인증 실패: API Key 또는 토큰을 확인하세요.")
        if resp.status_code == 403:
            raise PermissionError("권한 부족: 해당 작업을 수행할 권한이 없습니다.")
        if resp.status_code == 404:
            raise LookupError(f"리소스를 찾을 수 없습니다: {path}")
        if resp.status_code >= 400:
            detail = resp.json().get("detail", resp.text) if resp.text else resp.status_code
            raise RuntimeError(f"API 오류 ({resp.status_code}): {detail}")
        if resp.status_code == 204 or not resp.text:
            return None
        return resp.json()

    def get(self, path: str, **kwargs) -> Any:
        return self._request("GET", path, **kwargs)

    def post(self, path: str, data: dict | None = None, **kwargs) -> Any:
        return self._request("POST", path, json=data, **kwargs)

    def patch(self, path: str, data: dict | None = None, **kwargs) -> Any:
        return self._request("PATCH", path, json=data, **kwargs)

    def put(self, path: str, data: dict | None = None, **kwargs) -> Any:
        return self._request("PUT", path, json=data, **kwargs)

    def delete(self, path: str, **kwargs) -> Any:
        return self._request("DELETE", path, **kwargs)

    # ── 프로젝트 ──

    def list_projects(self) -> list[dict]:
        return self.get("/projects").get("items", [])

    def get_project(self, project_id: str) -> dict:
        return self.get(f"/projects/{project_id}")

    def create_project(self, name: str, description: str = "") -> dict:
        return self.post("/projects", {"name": name, "description": description})

    # ── 실험 ──

    def list_experiments(self, project_id: str) -> list[dict]:
        return self.get(f"/projects/{project_id}/experiments").get("items", [])

    def get_experiment(self, experiment_id: str) -> dict:
        return self.get(f"/experiments/{experiment_id}")

    def create_experiment(self, project_id: str, name: str, description: str = "") -> dict:
        return self.post(f"/projects/{project_id}/experiments", {"name": name, "description": description})

    # ── Run ──

    def create_run(self, experiment_id: str, name: str = "", tags: dict | None = None) -> dict:
        data = {"experiment_id": experiment_id}
        if name:
            data["name"] = name
        if tags:
            data["tags"] = tags
        return self.post("/runs", data)

    def get_run(self, run_id: str) -> dict:
        return self.get(f"/runs/{run_id}")

    def stop_run(self, run_id: str) -> dict:
        return self.post(f"/runs/{run_id}/stop")

    # ── 메트릭 ──

    def log_metric(self, run_id: str, key: str, value: float, step: int = 0, timestamp: int | None = None) -> None:
        data = {"key": key, "value": value, "step": step}
        if timestamp:
            data["timestamp"] = timestamp
        self.post(f"/runs/{run_id}/metrics", data)

    def log_metrics_batch(self, run_id: str, metrics: list[dict]) -> None:
        self.post(f"/runs/{run_id}/metrics/batch", {"metrics": metrics})

    def get_metric_history(self, run_id: str, key: str) -> list[dict]:
        return self.get(f"/runs/{run_id}/metrics/{key}/history")

    # ── 파라미터 ──

    def log_param(self, run_id: str, key: str, value: str) -> None:
        self.post(f"/runs/{run_id}/params", {"key": key, "value": str(value)})

    def log_params_batch(self, run_id: str, params: list[dict]) -> None:
        self.post(f"/runs/{run_id}/params/batch", {"params": params})

    # ── 태그 ──

    def set_tags(self, run_id: str, tags: dict) -> None:
        self.put(f"/runs/{run_id}/tags", {"tags": tags})

    # ── 아티팩트 ──

    def upload_artifact(self, run_id: str, local_path: str, artifact_path: str | None = None) -> dict:
        """로컬 파일을 Run의 아티팩트로 업로드"""
        import os
        url = self._url(f"/runs/{run_id}/artifacts")
        params = {}
        if artifact_path:
            params["path"] = artifact_path
        with open(local_path, "rb") as f:
            files = {"file": (os.path.basename(local_path), f)}
            resp = requests.post(url, headers={"Authorization": self._headers.get("Authorization", ""), "X-API-Key": self._headers.get("X-API-Key", "")}, files=files, params=params)
        if resp.status_code >= 400:
            raise RuntimeError(f"Artifact upload failed ({resp.status_code}): {resp.text[:200]}")
        return resp.json()

    def list_artifacts(self, run_id: str, path: str | None = None) -> list[dict]:
        params = {"path": path} if path else {}
        return self.get(f"/runs/{run_id}/artifacts", params=params)

    def download_artifact(self, run_id: str, artifact_path: str, local_path: str) -> str:
        """아티팩트를 로컬 파일로 다운로드"""
        url = self._url(f"/runs/{run_id}/artifacts/{artifact_path}/download")
        resp = requests.get(url, headers=self._headers)
        if resp.status_code >= 400:
            raise RuntimeError(f"Artifact download failed ({resp.status_code})")
        import os
        os.makedirs(os.path.dirname(local_path) if os.path.dirname(local_path) else ".", exist_ok=True)
        with open(local_path, "wb") as f:
            f.write(resp.content)
        return local_path

    # ── 모델 레지스트리 ──

    def list_models(self) -> list[dict]:
        return self.get("/models").get("items", [])

    def create_model(self, name: str, project_id: str, description: str = "") -> dict:
        return self.post("/models", {"name": name, "project_id": project_id, "description": description})

    def create_model_version(self, model_id: str, run_id: str = "", description: str = "") -> dict:
        data = {"description": description}
        if run_id:
            data["run_id"] = run_id
        return self.post(f"/models/{model_id}/versions", data)

    def set_model_alias(self, model_id: str, alias: str, version: int) -> None:
        self.put(f"/models/{model_id}/aliases/{alias}", {"version": version})
