"""
Bitbucket API client for repository management and change detection.
Uses Bitbucket Cloud REST API 2.0 with read-only access.
"""
import requests
import logging
import time
from typing import List, Dict, Optional
from datetime import datetime, timedelta
from urllib.parse import urlparse, parse_qs

logger = logging.getLogger(__name__)


class BitbucketClient:
    """Read-only client for interacting with Bitbucket Cloud API 2.0."""
    
    def __init__(self, workspace: str, username: str = None, app_password: str = None, api_token: str = None):
        """
        Initialize Bitbucket client with read-only access.
        
        Supports two authentication methods:
        1. App Password: username + app_password
        2. API Token: api_token (username will be your email if using token)
        
        Args:
            workspace: Bitbucket workspace name
            username: Bitbucket username/email (required for app password, optional for API token)
            app_password: Bitbucket app password (alternative to api_token)
            api_token: Atlassian API token (alternative to app_password)
        """
        self.workspace = workspace
        self.base_url = "https://api.bitbucket.org/2.0"
        
        # Determine authentication method
        if api_token:
            # Use API token - username should be email address
            if not username:
                raise ValueError("Username (email) is required when using API token")
            self.username = username
            self.api_token = api_token
            # API tokens use Basic Auth with email as username and token as password
            self.auth = (username, api_token)
        elif app_password:
            # Use app password
            if not username:
                raise ValueError("Username is required when using app password")
            self.username = username
            self.app_password = app_password
            self.auth = (username, app_password)
        else:
            raise ValueError("Either app_password or api_token must be provided")
        
        self.session = requests.Session()
        self.session.auth = self.auth
        # Rate limiting: respect API rate limits
        self.last_request_time = 0
        self.min_request_interval = 0.1  # 100ms between requests
        
    def _make_request(self, endpoint: str, params: Optional[Dict] = None, method: str = "GET") -> Dict:
        """
        Make authenticated read-only request to Bitbucket API 2.0.
        
        Args:
            endpoint: API endpoint (relative to base_url)
            params: Query parameters
            method: HTTP method (only GET allowed for read-only)
            
        Returns:
            JSON response data
            
        Raises:
            requests.RequestException: If request fails
        """
        # Ensure read-only - only GET requests
        if method.upper() != "GET":
            raise ValueError("Read-only client only supports GET requests")
        
        # Rate limiting
        time_since_last = time.time() - self.last_request_time
        if time_since_last < self.min_request_interval:
            time.sleep(self.min_request_interval - time_since_last)
        
        url = f"{self.base_url}/{endpoint.lstrip('/')}"
        
        try:
            response = self.session.get(url, params=params, timeout=30)
            self.last_request_time = time.time()
            
            # Handle rate limiting
            if response.status_code == 429:
                retry_after = int(response.headers.get("Retry-After", 60))
                logger.warning(f"Rate limited. Waiting {retry_after} seconds...")
                time.sleep(retry_after)
                response = self.session.get(url, params=params, timeout=30)
            
            response.raise_for_status()
            return response.json()
        except requests.HTTPError as e:
            # Don't log 404s as errors - they're expected when branches don't exist
            if e.response.status_code == 404:
                logger.debug(f"Bitbucket API 404 for {endpoint}: {e.response.reason}")
            else:
                logger.error(f"Bitbucket API request failed for {endpoint}: {e.response.status_code} {e.response.reason}")
            if hasattr(e.response, 'text'):
                logger.debug(f"Response: {e.response.text}")
            raise
        except requests.RequestException as e:
            logger.error(f"Bitbucket API request failed for {endpoint}: {e}")
            if hasattr(e, 'response') and hasattr(e.response, 'text'):
                logger.debug(f"Response: {e.response.text}")
            raise
    
    def get_all_repositories(self) -> List[Dict]:
        """
        Get all repositories in the workspace using API 2.0 pagination.
        
        Returns:
            List of repository dictionaries with name, slug, and clone URLs
        """
        repos = []
        endpoint = f"repositories/{self.workspace}"
        params = {"pagelen": 100}
        
        try:
            next_url = None
            while True:
                if next_url:
                    # Use the full next URL from API response (absolute URL)
                    # Extract endpoint and params from next URL
                    parsed = urlparse(next_url)
                    endpoint = parsed.path.replace("/2.0/", "").lstrip("/")
                    query_params = dict(parse_qs(parsed.query))
                    # Convert list values to single values for params
                    params = {k: v[0] if isinstance(v, list) and len(v) == 1 else v 
                             for k, v in query_params.items()}
                    data = self._make_request(endpoint, params)
                else:
                    # First request
                    data = self._make_request(endpoint, params)
                
                repos.extend(data.get("values", []))
                
                # Check for next page using API 2.0 pagination
                next_url = data.get("next")
                if not next_url:
                    break
            
            logger.info(f"Found {len(repos)} repositories in workspace {self.workspace}")
            return repos
        except Exception as e:
            logger.error(f"Failed to fetch repositories: {e}")
            return []
    
    def get_repository_info(self, repo_slug: str) -> Optional[Dict]:
        """
        Get detailed information about a specific repository.
        
        Args:
            repo_slug: Repository slug/name
            
        Returns:
            Repository information dictionary
        """
        try:
            endpoint = f"repositories/{self.workspace}/{repo_slug}"
            return self._make_request(endpoint)
        except Exception as e:
            logger.error(f"Failed to fetch repository info for {repo_slug}: {e}")
            return None
    
    def get_default_branch(self, repo_slug: str) -> Optional[str]:
        """
        Get the default branch for a repository.
        
        Args:
            repo_slug: Repository slug/name
            
        Returns:
            Default branch name (e.g., "main", "master") or None
        """
        try:
            repo_info = self.get_repository_info(repo_slug)
            if repo_info:
                # Try to get default branch from mainbranch field
                mainbranch = repo_info.get("mainbranch")
                if mainbranch:
                    branch_name = mainbranch.get("name") if isinstance(mainbranch, dict) else str(mainbranch)
                    if branch_name:
                        return branch_name
                
                # Fallback: try to get from links or other fields
                # Some repos have it in different places
                if "default_branch" in repo_info:
                    return repo_info["default_branch"]
        except Exception as e:
            logger.debug(f"Failed to get default branch for {repo_slug}: {e}")
        
        return None
    
    def get_latest_commit(self, repo_slug: str, branch: str = None) -> Optional[Dict]:
        """
        Get the latest commit for a repository branch using API 2.0.
        
        Args:
            repo_slug: Repository slug/name
            branch: Branch name (if None, will try to get default branch, then try main/master)
            
        Returns:
            Latest commit information with date
        """
        # Build list of branches to try
        branches_to_try = []
        
        # Add specified branch if provided
        if branch:
            branches_to_try.append(branch)
        
        # Try to get default branch from repository
        default_branch = self.get_default_branch(repo_slug)
        if default_branch and default_branch not in branches_to_try:
            branches_to_try.append(default_branch)
        
        # Add common defaults
        for common_branch in ["main", "master", "develop"]:
            if common_branch not in branches_to_try:
                branches_to_try.append(common_branch)
        
        for branch_name in branches_to_try:
            try:
                # Use API 2.0 commits endpoint with pagelen=1 to get only latest
                endpoint = f"repositories/{self.workspace}/{repo_slug}/commits/{branch_name}"
                params = {"pagelen": 1}
                data = self._make_request(endpoint, params)
                
                if data and "values" in data and len(data["values"]) > 0:
                    latest_commit = data["values"][0]
                    logger.debug(f"Found latest commit on branch '{branch_name}' for {repo_slug}")
                    return {
                        "hash": latest_commit.get("hash"),
                        "date": latest_commit.get("date"),
                        "message": latest_commit.get("message", ""),
                        "author": latest_commit.get("author", {}).get("raw", ""),
                        "branch": branch_name
                    }
            except requests.HTTPError as e:
                if e.response.status_code == 404:
                    # Branch doesn't exist, try next (log as debug, not error)
                    logger.debug(f"Branch '{branch_name}' not found for {repo_slug}, trying next branch")
                    continue
                # Other HTTP errors - log as warning
                logger.warning(f"HTTP error getting commits for branch '{branch_name}' in {repo_slug}: {e.response.status_code}")
                continue
            except Exception as e:
                # Non-HTTP errors - log as debug
                logger.debug(f"Error getting commits for branch '{branch_name}' in {repo_slug}: {e}")
                continue
        
        logger.warning(f"Could not find any valid branch for {repo_slug} (tried: {', '.join(branches_to_try)})")
        return None
    
    def has_changes_since(self, repo_slug: str, last_scan_date: Optional[datetime]) -> bool:
        """
        Check if repository has changes since last scan date.
        
        Args:
            repo_slug: Repository slug/name
            last_scan_date: Last scan datetime (None if never scanned)
            
        Returns:
            True if repository has new commits since last scan
        """
        if last_scan_date is None:
            return True  # Never scanned, so has changes
        
        latest_commit = self.get_latest_commit(repo_slug)
        if not latest_commit:
            return False
        
        commit_date_str = latest_commit.get("date")
        if not commit_date_str:
            return True  # Assume changes if we can't determine
        
        try:
            # Parse ISO 8601 date from Bitbucket
            commit_date = datetime.fromisoformat(commit_date_str.replace("Z", "+00:00"))
            # Convert to UTC if timezone-aware
            if commit_date.tzinfo:
                commit_date = commit_date.replace(tzinfo=None)
            
            return commit_date > last_scan_date
        except Exception as e:
            logger.warning(f"Failed to parse commit date: {e}")
            return True  # Assume changes on error
    
    def has_commits_in_last_n_days(self, repo_slug: str, days: int = 7, branch: str = None) -> bool:
        """
        Check if repository has at least one commit in the last N days (by latest commit).
        
        Args:
            repo_slug: Repository slug/name
            days: Number of days to look back (default: 7)
            branch: Branch name (None = default branch)
            
        Returns:
            True if latest commit is within the last N days
        """
        latest_commit = self.get_latest_commit(repo_slug, branch)
        if not latest_commit:
            return False
        
        commit_date_str = latest_commit.get("date")
        if not commit_date_str:
            return False
        
        try:
            commit_date = datetime.fromisoformat(commit_date_str.replace("Z", "+00:00"))
            if commit_date.tzinfo:
                commit_date = commit_date.replace(tzinfo=None)
            cutoff = datetime.now() - timedelta(days=days)
            return commit_date >= cutoff
        except Exception as e:
            logger.warning(f"Failed to parse commit date for {repo_slug}: {e}")
            return False
    
    def get_most_active_branch(self, repo_slug: str, days: int = 15) -> Optional[Dict]:
        """
        Get the branch with the most recent commit activity within the last N days.

        Uses Bitbucket API 2.0 branches endpoint with sort by latest commit date.
        Endpoint: /repositories/{workspace}/{repo_slug}/refs/branches?sort=-target.date

        Args:
            repo_slug: Repository slug/name
            days: Look-back window in days (default: 15)

        Returns:
            Dict with keys: name, date (ISO string), hash — or None if no activity in window
        """
        try:
            endpoint = f"repositories/{self.workspace}/{repo_slug}/refs/branches"
            params = {"sort": "-target.date", "pagelen": 1}
            data = self._make_request(endpoint, params)
            values = data.get("values", []) if data else []
            if not values:
                return None

            branch = values[0]
            name = branch.get("name")
            target = branch.get("target", {}) or {}
            date_str = target.get("date")
            commit_hash = target.get("hash")

            if not name or not date_str:
                return None

            commit_date = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
            if commit_date.tzinfo:
                commit_date = commit_date.replace(tzinfo=None)
            cutoff = datetime.now() - timedelta(days=days)
            if commit_date < cutoff:
                return None

            return {"name": name, "date": date_str, "hash": commit_hash}
        except requests.HTTPError as e:
            if e.response.status_code == 404:
                logger.debug(f"No branches found for {repo_slug}")
            else:
                logger.warning(f"HTTP error getting branches for {repo_slug}: {e.response.status_code}")
            return None
        except Exception as e:
            logger.debug(f"Error getting most active branch for {repo_slug}: {e}")
            return None

    def get_clone_url(self, repo_info: Dict, protocol: str = "https") -> Optional[str]:
        """
        Get clone URL for repository.
        
        Args:
            repo_info: Repository information dictionary
            protocol: Clone protocol (https or ssh)
            
        Returns:
            Clone URL string
        """
        links = repo_info.get("links", {})
        clone_links = links.get("clone", [])
        
        for link in clone_links:
            if link.get("name") == protocol:
                return link.get("href")
        
        return None

