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

import os
import stat
import subprocess
import threading
import time
from pathlib import Path
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.LOCAL.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.LOCAL.debug(f"SSH key 权限已正确 (600): {ssh_key_path}")
                return
        except Exception as e:
            logger.LOCAL.debug(f"无法检查 SSH key 权限: {e}")
        
        # 尝试设置权限为 600（仅所有者可读写）
        try:
            os.chmod(ssh_key_path, stat.S_IRUSR | stat.S_IWUSR)
            logger.LOCAL.debug(f"SSH key 权限已设置为 600: {ssh_key_path}")
        except OSError as e:
            # 如果是只读文件系统（挂载为只读），这是正常的，不影响使用
            if e.errno == 30:  # Read-only file system
                logger.LOCAL.debug(
                    f"SSH key 文件为只读挂载，无法修改权限（这是正常的）: {ssh_key_path}"
                )
            else:
                logger.LOCAL.warning(f"无法设置 SSH key 权限: {e}")
        except Exception as e:
            logger.LOCAL.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.LOCAL.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.LOCAL.info(f"Git 仓库不存在，开始克隆: {self.config.git_url}")
                self._clone_repository()
                # 记录拉取时间
                self._last_pull_time = time.time()
            else:
                logger.LOCAL.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.LOCAL.info("使用 SSH 协议克隆仓库")
            else:
                # HTTPS URL：注入 token
                git_url = self._inject_token_to_url(self.config.git_url)
                logger.LOCAL.info("使用 HTTPS 协议克隆仓库")

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

            logger.LOCAL.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.LOCAL.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.LOCAL.debug(f"没有配置任何远程仓库")
                return False
            
            # 分割成列表，过滤空字符串
            remotes = [r.strip() for r in remote_output.split('\n') if r.strip()]
            logger.LOCAL.debug(f"当前远程仓库列表: {remotes}")
            
            exists = remote_name in remotes
            logger.LOCAL.debug(f"远程仓库 '{remote_name}' {'存在' if exists else '不存在'}")
            return exists
        except GitOperationError as e:
            logger.LOCAL.warning(f"检查远程仓库失败: {e}")
            return False
        except Exception as e:
            logger.LOCAL.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.LOCAL.debug(f"当前已在目标分支 {target_branch} 上，无需切换")
            return

        if current_branch:
            logger.LOCAL.info(f"当前分支为 {current_branch}，切换到 {target_branch}")
        else:
            logger.LOCAL.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.LOCAL.warning("无法获取 commit hash，本地或远程分支信息不完整")
                return True  # 无法比较时，默认认为需要更新

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

        except Exception as e:
            logger.LOCAL.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.LOCAL.info("使用 SSH 协议更新仓库")
                else:
                    # HTTPS URL：注入 token
                    git_url = self._inject_token_to_url(self.config.git_url)
                    logger.LOCAL.info("使用 HTTPS 协议更新仓库")

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

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

                # 先获取远程信息（轻量fetch）
                logger.LOCAL.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.LOCAL.info(f"正在拉取代码（尝试 {attempt}/{max_retries}）...")

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

                logger.LOCAL.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.LOCAL.warning(
                            f"Git pull 失败（网络错误），{retry_delay}秒后重试... ({attempt}/{max_retries})"
                        )
                        logger.LOCAL.warning(f"错误信息: {error_msg}")
                        
                        # 尝试清理本地状态（可能有部分数据损坏）
                        try:
                            logger.LOCAL.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.LOCAL.info(f"执行命令: {log_cmd}")
        else:
            logger.LOCAL.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}@")

