"""Git 操作管理模块"""

import os
import stat
import subprocess
import threading
import time
from pathlib import Path

import snailjob as sj

from .config import PlaywrightExecutorConfig
from .exceptions import GitOperationError
from .logger import logger


class GitManager:
    """Git 操作管理器"""

    # 拉取缓存时间间隔（秒）
    PULL_CACHE_INTERVAL = 10

    def __init__(self, config: PlaywrightExecutorConfig):
        self.config = config
        self.repo_dir = config.git_repo_dir
        self._last_pull_time = 0.0  # 上次拉取时间戳
        self._pull_lock = threading.Lock()  # 保护拉取操作的锁

        # 如果使用 SSH URL，确保 SSH key 权限正确
        if config.is_ssh_url():
            self._ensure_ssh_key_permissions()

    def _ensure_ssh_key_permissions(self) -> None:
        """
        确保 SSH key 文件权限正确（600）
        这对于 SSH 认证是必需的
        """
        ssh_key_path = Path("/root/.ssh/id_rsa")
        
        if not ssh_key_path.exists():
            logger.REMOTE.warning(
                f"SSH key 文件不存在: {ssh_key_path}，"
                "请确保在 docker-compose.yml 中挂载了 ~/.ssh/id_rsa"
            )
            return
        
        # 检查当前权限
        try:
            current_mode = ssh_key_path.stat().st_mode
            # 检查是否为 600 (0o600 = 384 = S_IRUSR | S_IWUSR)
            expected_mode = stat.S_IRUSR | stat.S_IWUSR
            if (current_mode & 0o777) == expected_mode:
                # 权限已经是正确的，无需修改
                logger.REMOTE.debug(f"SSH key 权限已正确 (600): {ssh_key_path}")
                return
        except Exception as e:
            logger.REMOTE.debug(f"无法检查 SSH key 权限: {e}")
        
        # 尝试设置权限为 600（仅所有者可读写）
        try:
            os.chmod(ssh_key_path, stat.S_IRUSR | stat.S_IWUSR)
            logger.REMOTE.debug(f"SSH key 权限已设置为 600: {ssh_key_path}")
        except OSError as e:
            # 如果是只读文件系统（挂载为只读），这是正常的，不影响使用
            if e.errno == 30:  # Read-only file system
                logger.REMOTE.debug(
                    f"SSH key 文件为只读挂载，无法修改权限（这是正常的）: {ssh_key_path}"
                )
            else:
                logger.REMOTE.warning(f"无法设置 SSH key 权限: {e}")
        except Exception as e:
            logger.REMOTE.warning(f"无法设置 SSH key 权限: {e}")

    def _can_pull_now(self) -> bool:
        """
        检查是否可以立即进行拉取操作（基于缓存时间）

        Returns:
            True 如果可以拉取，False 如果在缓存时间内
        """
        current_time = time.time()
        time_since_last_pull = current_time - self._last_pull_time

        if time_since_last_pull < self.PULL_CACHE_INTERVAL:
            remaining_time = self.PULL_CACHE_INTERVAL - time_since_last_pull
            logger.REMOTE.info(
                f"拉取操作被缓存限制，还有 {remaining_time:.1f} 秒才能再次拉取"
            )
            return False

        return True

    def ensure_repository(self) -> None:
        """
        确保 Git 仓库存在且是最新的

        - 如果仓库不存在，执行 clone
        - 如果仓库存在，执行 pull 更新（受缓存时间限制，并发安全）
        """
        # 使用锁保护整个检查和拉取过程，确保并发安全
        with self._pull_lock:
            if not self.repo_dir.exists():
                logger.REMOTE.info(f"Git 仓库不存在，开始克隆: {self.config.git_url}")
                self._clone_repository()
                # 记录拉取时间
                self._last_pull_time = time.time()
            else:
                logger.REMOTE.info(f"Git 仓库已存在，开始更新: {self.repo_dir}")
                # 检查缓存时间
                if not self._can_pull_now():
                    return
                self._pull_repository()
                # 记录拉取时间
                self._last_pull_time = time.time()

    def _clone_repository(self) -> None:
        """克隆 Git 仓库（浅克隆）"""
        try:
            # 确保父目录存在
            self.repo_dir.parent.mkdir(parents=True, exist_ok=True)

            # 根据 URL 类型选择处理方式
            if self.config.is_ssh_url():
                # SSH URL：直接使用，不注入 token
                git_url = self.config.git_url
                logger.REMOTE.info("使用 SSH 协议克隆仓库")
            else:
                # HTTPS URL：注入 token
                git_url = self._inject_token_to_url(self.config.git_url)
                logger.REMOTE.info("使用 HTTPS 协议克隆仓库")

            # 执行 git clone（浅克隆，只克隆指定分支）
            cmd = [
                "git",
                "clone",
                "--branch",
                self.config.git_branch,
                "--depth",
                "1",  # 浅克隆，加快速度
                git_url,
                str(self.repo_dir),
            ]

            logger.REMOTE.info(f"执行命令: git clone --branch {self.config.git_branch} ...")

            result = subprocess.run(
                cmd, capture_output=True, text=True, timeout=300  # 5分钟超时
            )

            if result.returncode != 0:
                raise GitOperationError(f"Git clone 失败: {result.stderr}")

            logger.REMOTE.info("Git 仓库克隆成功")

        except subprocess.TimeoutExpired:
            raise GitOperationError("Git clone 超时（5分钟）")
        except Exception as e:
            raise GitOperationError(f"Git clone 异常: {str(e)}")

    def _get_commit_hash(self, ref: str) -> str:
        """
        获取指定引用（本地或远程）的 commit hash

        Args:
            ref: 引用名称，如 "HEAD" 或 "origin/main"

        Returns:
            commit hash 字符串
        """
        try:
            result = self._run_git_command(["rev-parse", ref], timeout=30)
            return result.stdout.strip()
        except GitOperationError:
            return ""

    def _get_current_branch(self) -> str:
        """
        获取当前所在的分支名称

        Returns:
            当前分支名称，如果获取失败返回空字符串
        """
        try:
            result = self._run_git_command(["branch", "--show-current"], timeout=30)
            return result.stdout.strip()
        except GitOperationError:
            return ""

    def _remote_exists(self, remote_name: str = "origin") -> bool:
        """
        检查指定的远程仓库是否存在

        Args:
            remote_name: 远程仓库名称，默认为 "origin"

        Returns:
            True 如果远程仓库存在，False 如果不存在
        """
        try:
            result = self._run_git_command(["remote"], timeout=30)
            remote_output = result.stdout.strip()
            
            # 如果输出为空，说明没有任何远程仓库
            if not remote_output:
                logger.REMOTE.debug(f"没有配置任何远程仓库")
                return False
            
            # 分割成列表，过滤空字符串
            remotes = [r.strip() for r in remote_output.split('\n') if r.strip()]
            logger.REMOTE.debug(f"当前远程仓库列表: {remotes}")
            
            exists = remote_name in remotes
            logger.REMOTE.debug(f"远程仓库 '{remote_name}' {'存在' if exists else '不存在'}")
            return exists
        except GitOperationError as e:
            logger.REMOTE.warning(f"检查远程仓库失败: {e}")
            return False
        except Exception as e:
            logger.REMOTE.warning(f"检查远程仓库异常: {e}")
            return False

    def _ensure_correct_branch(self) -> None:
        """
        确保当前在正确的分支上，如果不在则切换到指定分支
        """
        current_branch = self._get_current_branch()
        target_branch = self.config.git_branch

        if current_branch == target_branch:
            logger.REMOTE.debug(f"当前已在目标分支 {target_branch} 上，无需切换")
            return

        if current_branch:
            logger.REMOTE.info(f"当前分支为 {current_branch}，切换到 {target_branch}")
        else:
            logger.REMOTE.info(f"切换到分支 {target_branch}")

        self._run_git_command(["checkout", target_branch])

    def _is_remote_updated(self) -> bool:
        """
        检查远程是否有更新（需要在 fetch 后调用）

        Returns:
            True 如果远程有更新，False 如果本地已是最新
        """
        try:
            # 获取本地和远程的 commit hash（需要先 fetch）
            local_hash = self._get_commit_hash("HEAD")
            remote_hash = self._get_commit_hash(f"origin/{self.config.git_branch}")

            if not local_hash or not remote_hash:
                logger.REMOTE.warning("无法获取 commit hash，本地或远程分支信息不完整")
                return True  # 无法比较时，默认认为需要更新

            if local_hash == remote_hash:
                logger.REMOTE.info(f"本地已是最新版本 (commit: {local_hash[:8]})，跳过拉取")
                return False
            else:
                logger.REMOTE.info(f"检测到远程更新: {local_hash[:8]} -> {remote_hash[:8]}")
                return True

        except Exception as e:
            logger.REMOTE.warning(f"版本比较失败: {str(e)}，将继续执行拉取")
            return True  # 比较失败时，默认认为需要更新

    def _pull_repository(self) -> None:
        """更新 Git 仓库（带重试机制和回退策略）"""
        max_retries = 3
        retry_delay = 2  # 秒

        for attempt in range(1, max_retries + 1):
            try:
                # 根据 URL 类型选择处理方式
                if self.config.is_ssh_url():
                    # SSH URL：直接使用
                    git_url = self.config.git_url
                    logger.REMOTE.info("使用 SSH 协议更新仓库")
                else:
                    # HTTPS URL：注入 token
                    git_url = self._inject_token_to_url(self.config.git_url)
                    logger.REMOTE.info("使用 HTTPS 协议更新仓库")

                # 检查并设置 remote URL
                if self._remote_exists("origin"):
                    # origin 存在，更新 URL
                    logger.REMOTE.debug("更新 origin 远程仓库 URL")
                    self._run_git_command(
                        ["remote", "set-url", "origin", git_url], hide_token=True
                    )
                else:
                    # origin 不存在，添加
                    logger.REMOTE.info("origin 远程仓库不存在，正在添加...")
                    self._run_git_command(
                        ["remote", "add", "origin", git_url], hide_token=True
                    )

                # 确保在正确的分支上
                self._ensure_correct_branch()

                # 先获取远程信息（轻量fetch）
                logger.REMOTE.info(f"正在检查远程更新（尝试 {attempt}/{max_retries}）...")
                self._run_git_command(
                    ["fetch", "origin", self.config.git_branch, "--depth=1"], timeout=60
                )

                # 检查是否需要更新
                if not self._is_remote_updated():
                    return  # 本地已是最新版本，无需拉取

                # 拉取最新代码（增加超时时间到 300 秒）
                logger.REMOTE.info(f"正在拉取代码（尝试 {attempt}/{max_retries}）...")

                # 使用 reset 强制同步到远程分支，避免分支分歧问题
                # 这样即使远程被强制推送（force push）也不会出错
                self._run_git_command(
                    ["reset", "--hard", f"origin/{self.config.git_branch}"], timeout=60
                )

                logger.REMOTE.info("Git 仓库更新成功")
                return  # 成功后直接返回

            except GitOperationError as e:
                error_msg = str(e)
                
                # 如果是网络相关错误，进行重试
                if any(keyword in error_msg.lower() for keyword in [
                    'tls', 'connection', 'timeout', 'network', 'recv error', 'could not connect'
                ]):
                    if attempt < max_retries:
                        logger.REMOTE.warning(
                            f"Git pull 失败（网络错误），{retry_delay}秒后重试... ({attempt}/{max_retries})"
                        )
                        logger.REMOTE.warning(f"错误信息: {error_msg}")
                        
                        # 尝试清理本地状态（可能有部分数据损坏）
                        try:
                            logger.REMOTE.info("尝试清理 Git 状态...")
                            self._run_git_command(["reset", "--hard"], timeout=30)
                            self._run_git_command(["clean", "-fd"], timeout=30)
                        except:
                            pass  # 清理失败不影响重试
                        
                        import time
                        time.sleep(retry_delay)
                        retry_delay *= 2  # 指数退避
                        continue
                    else:
                        # 所有重试都失败，尝试删除仓库重新克隆
                        logger.REMOTE.error(
                            f"Git pull 失败（已重试{max_retries}次），将在下次执行时重新克隆仓库"
                        )
                        raise GitOperationError(
                            f"Git pull 失败（已重试{max_retries}次）: {error_msg}"
                        )
                else:
                    # 非网络错误，直接抛出
                    raise
                    
            except Exception as e:
                raise GitOperationError(f"Git pull 异常: {str(e)}")

    def _run_git_command(
        self, args: list, timeout: int = 60, hide_token: bool = False
    ) -> subprocess.CompletedProcess:
        """执行 Git 命令"""
        cmd = ["git", "-C", str(self.repo_dir)] + args
        
        # 日志输出（避免泄露 token）
        # 只有HTTPS URL且token不为空时才需要隐藏
        if hide_token and self.config.git_token and not self.config.is_ssh_url():
            # 隐藏带 token 的 URL
            import re
            log_cmd = " ".join(cmd)
            # GitHub格式: https://token@
            log_cmd = re.sub(r'https://[^@\s]+@', 'https://***@', log_cmd)
            # GitLab格式: https://oauth2:token@
            log_cmd = re.sub(r'https://oauth2:[^@\s]+@', 'https://oauth2:***@', log_cmd)
            logger.REMOTE.info(f"执行命令: {log_cmd}")
        else:
            logger.REMOTE.info(f"执行命令: {' '.join(cmd)}")

        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)

        if result.returncode != 0:
            # 错误信息中也要隐藏 token（仅HTTPS URL）
            stderr = result.stderr
            if self.config.git_token and not self.config.is_ssh_url():
                import re
                # 同样精确地隐藏错误信息中的token
                stderr = re.sub(r'https://[^@\s]+@', 'https://***@', stderr)
                stderr = re.sub(r'https://oauth2:[^@\s]+@', 'https://oauth2:***@', stderr)
            raise GitOperationError(f"Git 命令执行失败: {stderr}")

        return result

    def _inject_token_to_url(self, git_url: str) -> str:
        """
        将 token 注入到 Git URL 中（仅用于 HTTPS URL）

        支持格式:
            - GitHub: https://github.com/org/project.git
            - GitLab: https://gitlab.com/org/project.git
            - 自建 GitLab: https://gitlab.example.com/org/project.git
        
        认证格式:
            - GitHub: https://{token}@github.com/org/project.git
            - GitLab: https://oauth2:{token}@gitlab.com/org/project.git
        """
        if not git_url.startswith(("https://", "http://")):
            raise GitOperationError(
                f"不支持的 Git URL 格式: {git_url}，仅支持 HTTP(S) 协议"
            )
        
        # 判断是 GitHub 还是 GitLab
        is_github = "github.com" in git_url.lower()
        
        if is_github:
            # GitHub 使用 token 直接作为用户名
            if git_url.startswith("https://"):
                return git_url.replace("https://", f"https://{self.config.git_token}@")
            else:
                return git_url.replace("http://", f"http://{self.config.git_token}@")
        else:
            # GitLab 使用 oauth2:token 格式
            if git_url.startswith("https://"):
                return git_url.replace("https://", f"https://oauth2:{self.config.git_token}@")
            else:
                return git_url.replace("http://", f"http://oauth2:{self.config.git_token}@")

