"""云端同步模块 — 基于 Gitea REST API 的文件同步"""

import base64
import json
from typing import Optional, Tuple

import requests

from app.core.utils.log_utils import get_logger

logger = get_logger(__name__)


class GiteaSync:
    """通过 Gitea REST API 读写仓库文件"""

    def __init__(self, base_url: str, owner: str, repo: str, token: str, timeout: int = 15):
        self.base_url = base_url.rstrip("/")
        self.owner = owner
        self.repo = repo
        self.token = token
        self.timeout = timeout

    @property
    def _headers(self) -> dict:
        return {"Authorization": f"token {self.token}"}

    def _api_url(self, path: str) -> str:
        return f"{self.base_url}/api/v1/repos/{self.owner}/{self.repo}/contents/{path}"

    def pull_file(self, remote_path: str) -> Optional[Tuple[bytes, str]]:
        """从仓库下载文件

        Returns:
            (content_bytes, sha) 或 None（文件不存在或失败）
        """
        try:
            resp = requests.get(
                self._api_url(remote_path),
                headers=self._headers,
                timeout=self.timeout,
            )
            if resp.status_code == 404:
                logger.info(f"云端文件不存在: {remote_path}")
                return None
            if resp.status_code != 200:
                logger.warning(f"拉取文件失败: {resp.status_code} {resp.text[:200]}")
                return None

            data = resp.json()
            sha = data.get("sha", "")
            content_b64 = data.get("content", "")
            # Gitea 返回的 base64 可能含换行
            content_bytes = base64.b64decode(content_b64.replace("\n", ""))
            logger.info(f"拉取文件成功: {remote_path} ({len(content_bytes)} bytes)")
            return content_bytes, sha

        except requests.RequestException as e:
            logger.error(f"拉取文件网络错误: {e}")
            return None

    def push_file(
        self,
        remote_path: str,
        content: bytes,
        message: str,
        sha: Optional[str] = None,
    ) -> Optional[str]:
        """上传或更新文件到仓库

        Args:
            remote_path: 仓库中的文件路径
            content: 文件内容（bytes）
            message: commit message
            sha: 文件当前 sha（更新时必传，新建时省略）

        Returns:
            新的 sha，失败返回 None
        """
        payload = {
            "message": message,
            "content": base64.b64encode(content).decode("ascii"),
        }
        if sha:
            payload["sha"] = sha

        try:
            resp = requests.put(
                self._api_url(remote_path),
                headers={**self._headers, "Content-Type": "application/json"},
                json=payload,
                timeout=self.timeout,
            )
            if resp.status_code not in (200, 201):
                logger.warning(f"推送文件失败: {resp.status_code} {resp.text[:200]}")
                return None

            new_sha = resp.json().get("content", {}).get("sha", "")
            logger.info(f"推送文件成功: {remote_path} (sha={new_sha[:12]})")
            return new_sha

        except requests.RequestException as e:
            logger.error(f"推送文件网络错误: {e}")
            return None

    def file_exists(self, remote_path: str) -> Optional[str]:
        """检查文件是否存在

        Returns:
            文件 sha（存在）或 None（不存在）
        """
        try:
            resp = requests.head(
                self._api_url(remote_path),
                headers=self._headers,
                timeout=self.timeout,
            )
            if resp.status_code == 200:
                # HEAD 不返回 body，需要 GET 获取 sha
                result = self.pull_file(remote_path)
                return result[1] if result else None
            return None
        except requests.RequestException:
            return None

    def pull_json(self, remote_path: str) -> Optional[Tuple[dict, str]]:
        """拉取并解析 JSON 文件

        Returns:
            (parsed_dict, sha) 或 None
        """
        result = self.pull_file(remote_path)
        if result is None:
            return None
        content_bytes, sha = result
        try:
            data = json.loads(content_bytes)
            return data, sha
        except json.JSONDecodeError as e:
            logger.error(f"解析 JSON 失败: {e}")
            return None

    def push_json(self, remote_path: str, data: dict, message: str, sha: Optional[str] = None) -> Optional[str]:
        """将 dict 序列化为 JSON 并推送

        Returns:
            新的 sha，失败返回 None
        """
        content = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")
        return self.push_file(remote_path, content, message, sha)

    def push_binary(self, remote_path: str, local_path: str, message: str) -> Optional[str]:
        """读取本地二进制文件并推送到云端

        Returns:
            新的 sha，失败返回 None
        """
        try:
            with open(local_path, "rb") as f:
                content = f.read()
        except OSError as e:
            logger.error(f"读取本地文件失败: {local_path} — {e}")
            return None

        existing_sha = self.file_exists(remote_path)
        return self.push_file(remote_path, content, message, sha=existing_sha)

    def push(self) -> str:
        """推送本地数据到云端：product_cache.json + barcode_mappings.json"""
        import os
        from pathlib import Path

        project_root = Path(__file__).resolve().parent.parent.parent.parent

        results = []
        # 1. Product cache
        from app.core.db.product_db import ProductDatabase
        excel_source = str(project_root / "templates" / "商品资料.xlsx")
        db_path = str(project_root / "data" / "product_cache.db")
        product_db = ProductDatabase(db_path, excel_source)
        product_data = product_db.export_for_sync()
        sha = self.push_json("product_cache.json", product_data, "sync: update product cache")
        results.append(f"product_cache: {'ok' if sha else 'skip'}")

        # 2. Barcode mappings
        barcode_path = project_root / "config" / "barcode_mappings.json"
        if barcode_path.exists():
            with open(barcode_path, "r", encoding="utf-8") as f:
                barcode_data = json.loads(f.read())
            sha = self.push_json("barcode_mappings.json", barcode_data, "sync: update barcode mappings")
            results.append(f"barcode_mappings: {'ok' if sha else 'skip'}")

        return "; ".join(results) if results else "无数据需要同步"

    def pull(self) -> str:
        """从云端拉取数据并写入本地文件"""
        import os
        from pathlib import Path

        project_root = Path(__file__).resolve().parent.parent.parent.parent

        results = []
        # 1. Product cache
        result = self.pull_json("product_cache.json")
        if result is not None:
            data, sha = result
            from app.core.db.product_db import ProductDatabase
            excel_source = str(project_root / "templates" / "商品资料.xlsx")
            db_path = str(project_root / "data" / "product_cache.db")
            os.makedirs(os.path.dirname(db_path), exist_ok=True)
            product_db = ProductDatabase(db_path, excel_source)
            count = product_db.import_from_sync(data)
            results.append(f"product_cache: 导入 {count} 条")
        else:
            results.append("product_cache: 云端无数据")

        # 2. Barcode mappings
        barcode_result = self.pull_json("barcode_mappings.json")
        if barcode_result is not None:
            barcode_data, sha = barcode_result
            barcode_path = project_root / "config" / "barcode_mappings.json"
            barcode_path.parent.mkdir(parents=True, exist_ok=True)
            with open(barcode_path, "w", encoding="utf-8") as f:
                json.dump(barcode_data, f, ensure_ascii=False, indent=2)
            results.append(f"barcode_mappings: 已更新")
        else:
            results.append("barcode_mappings: 云端无数据")

        return "; ".join(results) if results else "无数据需要同步"

    @classmethod
    def from_config(cls, config) -> Optional["GiteaSync"]:
        """从 ConfigManager 创建实例

        Returns:
            GiteaSync 实例，配置不完整时返回 None
        """
        base_url = config.get("Gitea", "base_url", fallback="").strip()
        owner = config.get("Gitea", "owner", fallback="").strip()
        repo = config.get("Gitea", "repo", fallback="").strip()
        token = config.get("Gitea", "token", fallback="").strip()

        if not all([base_url, owner, repo, token]):
            logger.debug("Gitea 配置不完整，跳过云端同步")
            return None

        return cls(base_url=base_url, owner=owner, repo=repo, token=token)
