"""ZeroTier-backed Store implementation.

This store locates a service running on peers in a ZeroTier network and
proxies store operations to that remote service. The remote service can be
our `remote_storage_server` (default) or a Hydrus API server (`service=hydrus`).

Configuration keys:
- NAME: store instance name (required)
- NETWORK_ID: ZeroTier network ID to use for discovery (required)
- SERVICE: 'remote' or 'hydrus' (default: 'remote')
- PORT: service port (default: 999 for remote, 45869 for hydrus)
- API_KEY: optional API key to include in requests
- HOST: optional preferred peer address (skip discovery if provided)

Notes:
- This implementation focuses on read operations (search, get_file, get_metadata,
  tag/url ops). Uploads can be implemented later when the remote server
  supports a robust, authenticated upload endpoint.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from SYS.logger import debug, log
from Store._base import Store


class ZeroTier(Store):

    @classmethod
    def config_schema(cls) -> List[Dict[str, Any]]:
        return [
            {"key": "NAME", "label": "Store Name", "default": "", "required": True},
            {"key": "NETWORK_ID", "label": "ZeroTier Network ID", "default": "", "required": True},
            {"key": "HOST", "label": "Peer address (IP)", "default": "", "required": True},
            {"key": "PORT", "label": "Service Port", "default": "999", "required": False},
            {"key": "SERVICE", "label": "Service Type (remote|hydrus)", "default": "remote", "required": False},
            {"key": "API_KEY", "label": "API Key (optional)", "default": "", "required": False, "secret": True},
            {"key": "TIMEOUT", "label": "Request timeout (s)", "default": "5", "required": False},
        ]

    def __new__(cls, *args: Any, **kwargs: Any) -> "ZeroTier":
        inst = super().__new__(cls)
        name = kwargs.get("NAME")
        if name is not None:
            setattr(inst, "NAME", str(name))
        return inst

    def __init__(
        self,
        instance_name: Optional[str] = None,
        network_id: Optional[str] = None,
        service: Optional[str] = None,
        port: Optional[int] = None,
        api_key: Optional[str] = None,
        host: Optional[str] = None,
        timeout: Optional[int] = None,
        *,
        NAME: Optional[str] = None,
        NETWORK_ID: Optional[str] = None,
        SERVICE: Optional[str] = None,
        PORT: Optional[int] = None,
        API_KEY: Optional[str] = None,
        HOST: Optional[str] = None,
        TIMEOUT: Optional[int] = None,
    ) -> None:
        if instance_name is None and NAME is not None:
            instance_name = str(NAME)
        if network_id is None and NETWORK_ID is not None:
            network_id = str(NETWORK_ID)
        if service is None and SERVICE is not None:
            service = str(SERVICE)
        if port is None and PORT is not None:
            try:
                port = int(PORT)
            except Exception:
                port = None
        if api_key is None and API_KEY is not None:
            api_key = str(API_KEY)
        if host is None and HOST is not None:
            host = str(HOST)
        if timeout is None and TIMEOUT is not None:
            try:
                timeout = int(TIMEOUT)
            except Exception:
                timeout = None

        self._name = str(instance_name or "")
        self._network_id = str(network_id or "").strip()
        self._service = (str(service or "remote") or "remote").lower()
        self._port = int(port if port is not None else (45869 if self._service == "hydrus" else 999))
        self._api_key = str(api_key or "").strip() or None
        self._preferred_host = str(host or "").strip() or None
        self._timeout = int(timeout or 5)

        # Cached discovery result
        self._cached_peer: Optional[Tuple[str, int]] = None
        self._cached_client: Optional[Any] = None

    def name(self) -> str:
        return str(getattr(self, "_name", "zerotier"))

    # -------------------- internal helpers --------------------
    def _discover_peer(self, *, refresh: bool = False) -> Optional[Tuple[str, int]]:
        """Discover a peer host:port for this service on the configured network.

        Returns (host, port) or None.
        """
        if self._preferred_host and not refresh:
            return (self._preferred_host, self._port)

        if self._cached_peer and not refresh:
            return self._cached_peer

        try:
            from API import zerotier as zt
        except Exception as exc:
            debug(f"ZeroTier discovery helper not available: {exc}")
            return None

        # Try to find a central API key for better discovery
        from SYS.config import load_config

        conf = load_config()
        net_conf = conf.get("networking", {}).get("zerotier", {})
        central_token = net_conf.get("api_key")

        # Look for a matching service on the network
        probe = zt.find_peer_service(
            self._network_id,
            service_hint=("hydrus" if self._service == "hydrus" else None),
            port=self._port,
            api_token=central_token,
        )
        if probe:
            # Extract host:port
            host = probe.address
            port = probe.port or self._port
            self._cached_peer = (host, int(port))
            debug(f"ZeroTier store '{self.name()}' discovered peer {host}:{port}")
            return self._cached_peer

        debug(f"ZeroTier store '{self.name()}' found no peers on network {self._network_id}")
        return None

    def _ensure_client(self, *, refresh: bool = False) -> Optional[Any]:
        """Return a remote client object or base URL depending on service type.

        For 'hydrus' service we return an API.HydrusNetwork instance; for 'remote'
        service we return a base URL string to send HTTP requests to.
        """
        if self._cached_client and not refresh:
            return self._cached_client

        peer = self._discover_peer(refresh=refresh)
        if not peer:
            return None
        host, port = peer

        if self._service == "hydrus":
            try:
                from API.HydrusNetwork import HydrusNetwork as HydrusClient
                base_url = f"http://{host}:{port}"
                client = HydrusClient(url=base_url, access_key=(self._api_key or ""), timeout=self._timeout)
                self._cached_client = client
                return client
            except Exception as exc:
                debug(f"Failed to instantiate Hydrus client for ZeroTier peer {host}:{port}: {exc}")
                return None

        # Default: remote_storage 'http' style API
        self._cached_client = f"http://{host}:{port}"
        return self._cached_client

    def _request_remote(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None, json_body: Optional[Any] = None, timeout: Optional[int] = None) -> Optional[Any]:
        base = self._ensure_client()
        if base is None or not isinstance(base, str):
            debug("No remote base URL available for ZeroTier store")
            return None

        url = base.rstrip("/") + path
        headers = {}
        if self._api_key:
            headers["X-API-Key"] = self._api_key

        try:
            import httpx
            resp = httpx.request(method, url, params=params, json=json_body, headers=headers, timeout=timeout or self._timeout)
            if resp.status_code == 401:
                log(f"[Store={self._name}] Remote service at {url} requires an API Key. Please configure 'API_KEY' for this store.", severity="warning")
            resp.raise_for_status()
            try:
                return resp.json()
            except Exception:
                return resp.text
        except Exception as exc:
            debug(f"ZeroTier HTTP request failed: {method} {url} -> {exc}")
            return None

    # -------------------- Store API --------------------
    def search(self, query: str, **kwargs: Any) -> List[Dict[str, Any]]:
        """Search for files on the remote service."""
        client = self._ensure_client()
        if client is None:
            debug("ZeroTier search: no client available")
            return []

        if self._service == "hydrus":
            # Hydrus API expects tags list; best-effort: treat query as a single tag or raw search term
            try:
                tags = [query]
                payload = client.search_files(tags, return_hashes=True, return_file_ids=False, return_file_count=False)
                # Hydrus JSON shape varies; normalize to simple list
                files = []
                try:
                    if isinstance(payload, dict):
                        rows = payload.get("files") or payload.get("metadata") or []
                        for r in rows:
                            files.append(r if isinstance(r, dict) else {})
                except Exception:
                    pass
                return files
            except Exception as exc:
                debug(f"Hydrus search failed: {exc}")
                return []

        # remote_storage path
        params = {"q": query, "limit": int(kwargs.get("limit", 100))}
        res = self._request_remote("GET", "/files/search", params=params)
        if isinstance(res, dict):
            files = list(res.get("files") or [])
            # Inject store name and normalize keys for the CLI
            for f in files:
                if isinstance(f, dict):
                    f["store"] = self._name
                    # remote_storage_server returns 'file_path' and 'size'
                    # CLI prefers 'path' and 'size_bytes'
                    if "file_path" in f and "path" not in f:
                        f["path"] = f["file_path"]
                        
                        # Try to extract title from tags
                        tags = f.get("tag") or []
                        title_tag = next((t for t in tags if str(t).lower().startswith("title:")), None)
                        if title_tag and ":" in title_tag:
                            f["title"] = title_tag.split(":", 1)[1].strip()
                        elif "title" not in f:
                            try:
                                f["title"] = Path(f["file_path"]).stem
                            except Exception:
                                f["title"] = f["file_path"]
                                
                    if "size" in f and "size_bytes" not in f:
                        f["size_bytes"] = f["size"]
            return files
        return []

    def get_file(self, file_hash: str, **kwargs: Any) -> Optional[Path | str]:
        """Return either a URL (hydrus or remote capable) or local path (not implemented).

        For Hydrus: return the direct file URL (Hydrus client URL with access token appended if needed).
        For remote_storage: currently return the metadata path (if available) or None.
        """
        client = self._ensure_client()
        if client is None:
            return None

        if self._service == "hydrus":
            try:
                # Hydrus wrapper provides file_url() convenience
                return client.file_url(file_hash)
            except Exception as exc:
                debug(f"Hydrus get_file failed: {exc}")
                return None

        # remote storage: return download URL
        base = self._ensure_client()
        if not base or not isinstance(base, str):
            return None
        
        url = f"{base.rstrip('/')}/files/raw/{file_hash}"
        if self._api_key:
            sep = "&" if "?" in url else "?"
            url += f"{sep}api_key={self._api_key}"
        return url

    def download_to_temp(
        self,
        file_hash: str,
        temp_root: Optional[Path] = None,
        suffix: Optional[str] = None,
        progress_callback: Optional[Callable[[int, int], None]] = None,
    ) -> Optional[Path]:
        """Download a file from the remote peer to a local temporary file."""
        import os
        import httpx
        import tempfile

        if self._service == "hydrus":
            return None

        url = self.get_file(file_hash)
        if not url or not isinstance(url, str) or not url.startswith("http"):
            return None

        # Ensure suffix starts with a dot if provided
        if suffix and not suffix.startswith("."):
            suffix = f".{suffix}"
        if not suffix:
            suffix = ".tmp"

        try:
            # Use provided temp_root or system temp
            if temp_root:
                temp_root.mkdir(parents=True, exist_ok=True)
                fd, tmp_path = tempfile.mkstemp(dir=str(temp_root), suffix=suffix)
            else:
                fd, tmp_path = tempfile.mkstemp(suffix=suffix)

            os_fd = os.fdopen(fd, "wb")

            headers = {}
            if self._api_key:
                headers["X-API-Key"] = self._api_key

            downloaded = 0
            total = 0
            with httpx.stream("GET", url, headers=headers, timeout=self._timeout) as r:
                r.raise_for_status()
                total = int(r.headers.get("Content-Length", 0))
                # Use a larger chunk size for ZeroTier/P2P efficiency
                for chunk in r.iter_bytes(chunk_size=128 * 1024):
                    if chunk:
                        os_fd.write(chunk)
                        downloaded += len(chunk)
                        if progress_callback:
                            try:
                                progress_callback(downloaded, total)
                            except Exception:
                                pass

            os_fd.close()
            return Path(tmp_path)

        except Exception as exc:
            debug(f"ZeroTier download_to_temp failed for {file_hash}: {exc}")
            return None

    def add_file(self, file_path: Path, **kwargs: Any) -> Optional[str]:
        """Upload a local file to the remote ZeroTier peer (supports 'remote' and 'hydrus' services).

        Returns the file hash on success, or None on failure.
        """

        p = Path(file_path)
        if not p.exists():
            debug(f"ZeroTier add_file: local file not found: {p}")
            return None

        # Hydrus: delegate to Hydrus client add_file()
        if self._service == "hydrus":
            try:
                client = self._ensure_client()
                if client is None:
                    debug("ZeroTier add_file: Hydrus client unavailable")
                    return None
                return client.add_file(p, **kwargs)
            except Exception as exc:
                debug(f"ZeroTier hydrus add_file failed: {exc}")
                return None

        # Remote server: POST /files/upload multipart/form-data
        base = self._ensure_client()
        if base is None or not isinstance(base, str):
            debug("ZeroTier add_file: no remote base URL available")
            return None

        url = base.rstrip("/") + "/files/upload"
        headers = {}
        if self._api_key:
            headers["X-API-Key"] = self._api_key

        try:
            import httpx
            with open(p, "rb") as fh:
                # Build form fields for tags/urls (support list or comma-separated)
                data = []
                if "tag" in kwargs:
                    tags = kwargs.get("tag") or []
                    if isinstance(tags, str):
                        tags = [t.strip() for t in tags.split(",") if t.strip()]
                    for t in tags:
                        data.append(("tag", t))
                if "url" in kwargs:
                    urls = kwargs.get("url") or []
                    if isinstance(urls, str):
                        urls = [u.strip() for u in urls.split(",") if u.strip()]
                    for u in urls:
                        data.append(("url", u))

                files = {"file": (p.name, fh, "application/octet-stream")}
                # Prefer `requests` for local testing / WSGI servers which may not accept
                # chunked uploads reliably with httpx/httpcore. Fall back to httpx otherwise.
                try:
                    try:
                        import requests
                        # Convert data list-of-tuples to dict for requests (acceptable for repeated fields)
                        data_dict = {}
                        for k, v in data:
                            if k in data_dict:
                                existing = data_dict[k]
                                if not isinstance(existing, list):
                                    data_dict[k] = [existing]
                                data_dict[k].append(v)
                            else:
                                data_dict[k] = v
                        r = requests.post(url, headers=headers, files=files, data=data_dict or None, timeout=self._timeout)
                        if r.status_code in (200, 201):
                            try:
                                payload = r.json()
                                file_hash = payload.get("hash") or payload.get("file_hash")
                                return file_hash
                            except Exception:
                                return None
                        try:
                            debug(f"[zerotier-debug] upload failed (requests) status={r.status_code} body={r.text}")
                        except Exception:
                            pass
                        debug(f"ZeroTier add_file failed (requests): status {r.status_code} body={getattr(r, 'text', '')}")
                        return None
                    except Exception:
                        import httpx
                        resp = httpx.post(url, headers=headers, files=files, data=data, timeout=self._timeout)
                        # Note: some environments may not create request.files correctly; capture body for debugging
                        try:
                            if resp.status_code in (200, 201):
                                try:
                                    payload = resp.json()
                                    file_hash = payload.get("hash") or payload.get("file_hash")
                                    return file_hash
                                except Exception:
                                    return None
                            # Debug output to help tests capture server response
                            try:
                                debug(f"[zerotier-debug] upload failed status={resp.status_code} body={resp.text}")
                            except Exception:
                                pass
                            debug(f"ZeroTier add_file failed: status {resp.status_code} body={getattr(resp, 'text', '')}")
                            return None
                        except Exception as exc:
                            debug(f"ZeroTier add_file exception: {exc}")
                            return None
                except Exception as exc:
                    debug(f"ZeroTier add_file exception: {exc}")
                    return None
        except Exception as exc:
            debug(f"ZeroTier add_file exception: {exc}")
            return None

    def get_metadata(self, file_hash: str, **kwargs: Any) -> Optional[Dict[str, Any]]:
        client = self._ensure_client()
        if client is None:
            return None

        if self._service == "hydrus":
            try:
                payload = client.fetch_file_metadata(hashes=[file_hash], include_file_url=True, include_size=True, include_mime=True)
                return payload
            except Exception as exc:
                debug(f"Hydrus fetch_file_metadata failed: {exc}")
                return None

        res = self._request_remote("GET", f"/files/{file_hash}")
        if isinstance(res, dict):
            # Extract title from tags for the details panel/metadata view
            tags = res.get("tag") or []
            title_tag = next((t for t in tags if str(t).lower().startswith("title:")), None)
            if title_tag and ":" in title_tag:
                res["title"] = title_tag.split(":", 1)[1].strip()
            return res
        return None

    def get_tag(self, file_identifier: str, **kwargs: Any) -> Tuple[List[str], str]:
        # Return (tags, service). For hydrus use fetch_file_metadata service keys.
        client = self._ensure_client()
        if client is None:
            return ([], "")

        if self._service == "hydrus":
            try:
                payload = client.fetch_file_metadata(hashes=[file_identifier], include_service_keys_to_tags=True)
                tags = []
                if isinstance(payload, dict):
                    metas = payload.get("metadata") or []
                    if metas and isinstance(metas, list) and metas:
                        md = metas[0]
                        if isinstance(md, dict):
                            tags = md.get("service_keys_to_tags") or []
                return (tags, "hydrus")
            except Exception as exc:
                debug(f"Hydrus get_tag failed: {exc}")
                return ([], "hydrus")

        res = self._request_remote("GET", f"/tags/{file_identifier}")
        if isinstance(res, dict):
            return (list(res.get("tag") or []), "remote")
        return ([], "remote")

    def add_tag(self, file_identifier: str, tags: List[str], **kwargs: Any) -> bool:
        client = self._ensure_client()
        if client is None:
            return False

        if self._service == "hydrus":
            try:
                service_name = kwargs.get("service_name") or "my tags"
                client.add_tag(file_identifier, tags, service_name)
                return True
            except Exception as exc:
                debug(f"Hydrus add_tag failed: {exc}")
                return False

        payload = {"tag": tags}
        res = self._request_remote("POST", f"/tags/{file_identifier}", json_body=payload)
        return bool(res)

    def delete_tag(self, file_identifier: str, tags: List[str], **kwargs: Any) -> bool:
        client = self._ensure_client()
        if client is None:
            return False

        if self._service == "hydrus":
            try:
                service_name = kwargs.get("service_name") or "my tags"
                client.delete_tag(file_identifier, tags, service_name)
                return True
            except Exception as exc:
                debug(f"Hydrus delete_tag failed: {exc}")
                return False

        # remote_storage DELETE /tags/<hash>?tag=tag1,tag2
        query = {"tag": ",".join(tags)}
        res = self._request_remote("DELETE", f"/tags/{file_identifier}", params=query)
        return bool(res)

    def get_url(self, file_identifier: str, **kwargs: Any) -> List[str]:
        # For Hydrus, use fetch_file_metadata to include file URL; for remote, GET tags endpoint includes urls
        client = self._ensure_client()
        if client is None:
            return []

        if self._service == "hydrus":
            try:
                payload = client.fetch_file_metadata(hashes=[file_identifier], include_file_url=True)
                try:
                    metas = payload.get("metadata") or []
                    if metas and isinstance(metas, list) and metas:
                        md = metas[0]
                        if isinstance(md, dict):
                            urls = md.get("file_urls") or []
                            return list(urls)
                except Exception:
                    pass
                return []
            except Exception as exc:
                debug(f"Hydrus get_url failed: {exc}")
                return []

        meta = self._request_remote("GET", f"/files/{file_identifier}")
        if isinstance(meta, dict):
            urls = meta.get("url") or []
            return list(urls)
        return []

    def add_url(self, file_identifier: str, url: List[str], **kwargs: Any) -> bool:
        client = self._ensure_client()
        if client is None:
            return False

        if self._service == "hydrus":
            try:
                client.associate_url(hashes=[file_identifier], url=url[0])
                return True
            except Exception as exc:
                debug(f"Hydrus add_url failed: {exc}")
                return False

        payload = {"url": url}
        res = self._request_remote("POST", f"/files/{file_identifier}/url", json_body=payload)
        return bool(res)

    def delete_url(self, file_identifier: str, url: List[str], **kwargs: Any) -> bool:
        client = self._ensure_client()
        if client is None:
            return False

        if self._service == "hydrus":
            try:
                client.delete_urls(hashes=[file_identifier], urls=url)
                return True
            except Exception as exc:
                debug(f"Hydrus delete_url failed: {exc}")
                return False

        payload = {"url": url}
        res = self._request_remote("DELETE", f"/files/{file_identifier}/url", json_body=payload)
        return bool(res)

    def get_note(self, file_identifier: str, **kwargs: Any) -> Dict[str, str]:
        """Get named notes for a file. Returns a mapping of name->text."""
        client = self._ensure_client()
        if client is None:
            return {}

        if self._service == "hydrus":
            try:
                # Hydrus API may expose notes via fetch_file_metadata; best-effort
                payload = client.fetch_file_metadata(hashes=[file_identifier], include_notes=True)
                if isinstance(payload, dict):
                    metas = payload.get("metadata") or []
                    if metas and isinstance(metas, list):
                        md = metas[0]
                        notes = md.get("notes") or {}
                        return dict(notes)
            except Exception:
                return {}

        # Remote storage has no notes API yet
        return {}

    def set_note(self, file_identifier: str, name: str, text: str, **kwargs: Any) -> bool:
        client = self._ensure_client()
        if client is None:
            return False

        if self._service == "hydrus":
            try:
                client.set_note(file_identifier, name, text)
                return True
            except Exception:
                return False

        # Remote storage: not supported
        return False

    def delete_note(self, file_identifier: str, name: str, **kwargs: Any) -> bool:
        client = self._ensure_client()
        if client is None:
            return False

        if self._service == "hydrus":
            try:
                client.delete_note(file_identifier, name)
                return True
            except Exception:
                return False

        return False
