#!/usr/bin/env python3
# 中文注释：
# 文件：echobot/skills/siyuan/scripts/client.py
# 说明：SiYuan API 客户端，封装基础 HTTP 调用与常用查询接口。
"""
SiYuan API Client
"""

import json
import os
import urllib.request
import urllib.error
from urllib.parse import urlparse
from typing import Any, Dict, Optional


class SiYuanClient:
    """Client for SiYuan Notes API"""
    
    def __init__(self, host: str = None, port: int | str = None, token: str = None, scheme: str = None):
        env_host = os.getenv("SIYUAN_HOST", "localhost")
        env_port = os.getenv("SIYUAN_PORT", "6806")
        env_scheme = os.getenv("SIYUAN_SCHEME", "").strip().lower()

        self.host = (host or env_host).strip()
        self.port = (port or env_port or "").strip()
        self.token = token or os.getenv("SIYUAN_TOKEN")
        self.scheme = (scheme or env_scheme or "http").strip()

        parsed = urlparse(self.host)
        if parsed.scheme:
            # Host already contains scheme, use it directly
            self.base_url = self.host.rstrip("/")
        else:
            port_suffix = f":{self.port}" if self.port else ""
            self.base_url = f"{self.scheme}://{self.host}{port_suffix}"

        if not self.token:
            raise ValueError("SIYUAN_TOKEN is required")
    
    @staticmethod
    def _normalize_limit(limit: int, default: int, max_value: int = 1000) -> int:
        """Normalize limit value to a safe integer range."""
        try:
            value = int(limit)
        except (TypeError, ValueError):
            value = default
        return max(1, min(value, max_value))

    @staticmethod
    def _escape_sql(value: str) -> str:
        """Escape SQL string literal for SQLite."""
        return value.replace("'", "''")

    def _request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """Make API request to SiYuan using stdlib to avoid extra deps."""
        url = f"{self.base_url}{endpoint}"
        headers = {
            "Authorization": f"Token {self.token}",
            "Content-Type": "application/json",
        }

        body = json.dumps(params or {}).encode("utf-8")
        request = urllib.request.Request(
            url=url,
            data=body,
            headers=headers,
            method="POST",
        )

        try:
            with urllib.request.urlopen(request, timeout=30) as response:
                raw = response.read()
                try:
                    return json.loads(raw)
                except json.JSONDecodeError as exc:
                    snippet = raw.decode("utf-8", errors="ignore").strip().replace("\n", " ")[:200]
                    raise RuntimeError(
                        f"Invalid JSON response from {endpoint}: {snippet or '<empty>'}"
                    ) from exc
        except urllib.error.HTTPError as exc:  # type: ignore[attr-defined]
            snippet = exc.read().decode("utf-8", errors="ignore").strip().replace("\n", " ")[:200]
            raise RuntimeError(f"HTTP {exc.code} from {endpoint}: {snippet}") from exc
        except urllib.error.URLError as exc:  # type: ignore[attr-defined]
            raise RuntimeError(f"Request failed: {exc.reason}") from exc
    
    def search(self, query: str, limit: int = 20, type: str = "blocks") -> Dict[str, Any]:
        """Search blocks/documents via SQL fallback for API compatibility."""
        safe_query = self._escape_sql((query or "").strip())
        if not safe_query:
            return {"code": 0, "msg": "", "data": []}

        limit_value = self._normalize_limit(limit, default=20, max_value=200)
        doc_filter = "type = 'd' AND " if type == "notes" else ""
        stmt = (
            "SELECT id, type, content, hpath, updated "
            "FROM blocks "
            f"WHERE {doc_filter}content LIKE '%{safe_query}%' "
            "ORDER BY updated DESC "
            f"LIMIT {limit_value}"
        )
        return self.query_sql(stmt)
    
    def get_notebooks(self) -> Dict[str, Any]:
        """Get all notebooks"""
        return self._request("/api/notebook/lsNotebooks", {})
    
    def get_recent_docs(self, limit: int = 10) -> Dict[str, Any]:
        """Get recent docs via SQL fallback for API compatibility."""
        limit_value = self._normalize_limit(limit, default=10, max_value=100)
        stmt = (
            "SELECT id, type, content, hpath, updated "
            "FROM blocks "
            "WHERE type = 'd' "
            "ORDER BY updated DESC "
            f"LIMIT {limit_value}"
        )
        return self.query_sql(stmt)
    
    def get_dailies(self, start_date: str, end_date: str) -> Dict[str, Any]:
        """Get docs and filter date range in tool layer."""
        # The official API does not expose a stable "get dailies" endpoint.
        # Return candidate doc rows and let the caller apply date-range filtering.
        stmt = (
            "SELECT id, type, content, hpath, updated "
            "FROM blocks "
            "WHERE type = 'd' "
            "ORDER BY updated DESC "
            "LIMIT 2000"
        )
        return self.query_sql(stmt)
    
    def get_document(self, doc_id: str) -> Dict[str, Any]:
        """Get document markdown content."""
        return self._request("/api/export/exportMdContent", {"id": doc_id})
    
    def create_doc(self, notebook_id: str, path: str, title: str, content: str = "") -> Dict[str, Any]:
        """Create document"""
        normalized_path = (path or "/").strip()
        if not normalized_path.startswith("/"):
            normalized_path = f"/{normalized_path}"
        if normalized_path.endswith("/"):
            normalized_path = f"{normalized_path}{title}"

        body = (content or "").strip()
        if not body:
            markdown = f"# {title}\n"
        elif body.lstrip().startswith("#"):
            markdown = body
        else:
            markdown = f"# {title}\n\n{body}"

        return self._request("/api/filetree/createDocWithMd", {
            "notebook": notebook_id,
            "path": normalized_path,
            "markdown": markdown,
        })
    
    def update_doc(self, doc_id: str, content: str) -> Dict[str, Any]:
        """Update document"""
        return self._request("/api/block/updateBlock", {
            "id": doc_id,
            "dataType": "markdown",
            "data": content
        })
    
    def query_sql(self, sql: str) -> Dict[str, Any]:
        """Execute SQL query"""
        stmt = (sql or "").strip()
        if not stmt:
            return {"code": 0, "msg": "", "data": []}
        return self._request("/api/query/sql", {"stmt": stmt})
    
    def query_blocks(self, hsql: str, limit: int = 100) -> Dict[str, Any]:
        """Query blocks using SQL-compatible statement."""
        stmt = (hsql or "").strip()
        if not stmt:
            return {"code": 0, "msg": "", "data": []}

        limit_value = self._normalize_limit(limit, default=100, max_value=1000)
        if " limit " not in f" {stmt.lower()} ":
            stmt = f"{stmt.rstrip(';')} LIMIT {limit_value}"
        return self.query_sql(stmt)
