"""
Generic Jira Connector
A flexible Python connector for interacting with Jira API
"""

import requests
import logging
from typing import Dict, List, Any
import time


class JiraConnector:
    """
    Generic Jira connector that can connect to any Jira instance
    and read specified properties
    """

    def __init__(
        self,
        jira_url: str,
        token: str,
        username: str,
        service: str = "jira",
        timeout: int = 30,
        verify_ssl: bool = True,
        max_retries: int = 3,
    ):
        """
        Initialize Jira connector

        Args:
            jira_url: Base URL of Jira instance
                (e.g., "https://yourcompany.atlassian.net")
            token: API token for authentication
            service: Service type for configuration (default: "jira")
            username: Username for basic auth (optional, token-based auth preferred)
            timeout: Request timeout in seconds
            verify_ssl: Whether to verify SSL certificates
            max_retries: Maximum number of retry attempts for failed requests
        """
        self.jira_url = jira_url.rstrip("/")
        self.token = token
        self.service = service
        self.username = username
        self.timeout = timeout
        self.verify_ssl = verify_ssl
        self.max_retries = max_retries

        # Setup logging
        self.logger = logging.getLogger(f"JiraConnector_{service}")
        if not self.logger.handlers:
            handler = logging.StreamHandler()
            formatter = logging.Formatter(
                "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
            )
            handler.setFormatter(formatter)
            self.logger.addHandler(handler)
            self.logger.setLevel(logging.INFO)

        # Initialize session
        self.session = requests.Session()
        self._setup_authentication()

        # API endpoints
        self.api_base = f"{self.jira_url}/rest/api/2"

        self.logger.info(f"Jira connector initialized for {service} at {jira_url}")

    def _setup_authentication(self):
        """Setup authentication headers"""
        # Bearer token authentication (preferred)
        self.session.headers.update(
            {
                "Authorization": f"Bearer {self.token}",
                "Accept": "application/json",
                "Content-Type": "application/json",
            }
        )
        self.logger.info("Using bearer token authentication")

    def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
        """
        Make HTTP request with retry logic

        Args:
            method: HTTP method (GET, POST, PUT, DELETE)
            endpoint: API endpoint
            **kwargs: Additional request parameters

        Returns:
            Response data as dictionary

        Raises:
            Exception: If request fails after max retries
        """
        url = (
            endpoint
            if endpoint.startswith("http")
            else f"{self.api_base}/{endpoint.lstrip('/')}"
        )

        for attempt in range(self.max_retries + 1):
            try:
                response = self.session.request(
                    method=method,
                    url=url,
                    timeout=self.timeout,
                    verify=self.verify_ssl,
                    **kwargs,
                )

                response.raise_for_status()

                if response.content:
                    return response.json()
                else:
                    return {}

            except requests.exceptions.RequestException as e:
                if attempt == self.max_retries:
                    self.logger.error(
                        f"Request failed after {self.max_retries} attempts: {e}"
                    )
                    raise Exception(f"Failed to make request to {url}: {e}")

                wait_time = 2**attempt
                self.logger.warning(
                    f"Request failed (attempt {attempt + 1}), retrying in "
                    f"{wait_time}s: {e}"
                )
                time.sleep(wait_time)

    def test_connection(self) -> bool:
        """
        Test connection to Jira

        Returns:
            True if connection successful, False otherwise
        """
        try:
            self._make_request("GET", "/myself")
            self.logger.info("Connection test successful")
            return True
        except Exception as e:
            self.logger.error(f"Connection test failed: {e}")
            return False

    def get_issue(
        self, issue_key: str, properties: List[str] = None, format_values: bool = True
    ) -> Dict[str, Any]:
        """
        Get issue details with specified properties

        Args:
            issue_key: Issue key (e.g., "PROJ-123")
            properties: List of properties to extract (None for all)
            format_values: Whether to format complex values for readability

        Returns:
            Dictionary containing issue data with requested properties
        """
        try:
            # Get full issue data
            issue_data = self._make_request("GET", f"/issue/{issue_key}")

            if properties is None:
                return issue_data

            # Extract only requested properties
            result = {"key": issue_key}

            for prop in properties:
                value = self._extract_property(issue_data, prop)
                if value is not None:
                    if format_values:
                        value = self._format_value(value)
                    result[prop] = value

            return result

        except Exception as e:
            self.logger.error(f"Failed to get issue {issue_key}: {e}")
            raise

    def _extract_property(self, data: Dict[str, Any], property_path: str) -> Any:
        """
        Extract property from nested dictionary using dot notation

        Args:
            data: Source dictionary
            property_path: Property path
                (e.g., "fields.summary" or "fields.customfield_10010")

        Returns:
            Property value or None if not found
        """
        keys = property_path.split(".")
        current = data

        try:
            for key in keys:
                if isinstance(current, dict):
                    current = current.get(key)
                elif isinstance(current, list) and key.isdigit():
                    current = current[int(key)]
                else:
                    return None

                if current is None:
                    return None

            return current
        except (KeyError, IndexError, TypeError):
            return None

    def _format_value(self, value: Any) -> Any:
        """
        Format value for better readability

        Args:
            value: Value to format

        Returns:
            Formatted value
        """
        if value is None:
            return None

        if isinstance(value, dict):
            if "displayName" in value:
                return value["displayName"]
            elif "name" in value:
                return value["name"]
            elif "value" in value:
                return value["value"]
            return value

        if isinstance(value, list):
            return [self._format_value(item) for item in value]

        return value

    def search_issues(
        self,
        jql: str,
        properties: List[str] = None,
        max_results: int = 50,
        start_at: int = 0,
        format_values: bool = True,
    ) -> Dict[str, Any]:
        """
        Search issues using JQL

        Args:
            jql: JQL query string
            properties: List of properties to extract (None for all)
            max_results: Maximum number of results to return
            start_at: Starting index for pagination
            format_values: Whether to format complex values for readability

        Returns:
            Dictionary containing search results
        """
        try:
            params = {
                "jql": jql,
                "maxResults": max_results,
                "startAt": start_at,
                "fields": ["*all"] if properties is None else properties,
            }

            response = self._make_request("GET", "/search", params=params)

            if properties is not None:
                # Extract only requested properties for each issue
                issues = []
                for issue in response.get("issues", []):
                    filtered_issue = {"key": issue["key"]}
                    for prop in properties:
                        value = self._extract_property(issue, prop)
                        if value is not None:
                            if format_values:
                                value = self._format_value(value)
                            filtered_issue[prop] = value
                    issues.append(filtered_issue)
                response["issues"] = issues

            return response

        except Exception as e:
            self.logger.error(f"Failed to search issues with JQL '{jql}': {e}")
            raise

    def get_projects(self) -> List[Dict[str, Any]]:
        """
        Get all accessible projects

        Returns:
            List of project dictionaries
        """
        try:
            response = self._make_request("GET", "/project")
            return response
        except Exception as e:
            self.logger.error(f"Failed to get projects: {e}")
            raise

    def get_project_issues(
        self,
        project_key: str,
        properties: List[str] = None,
        max_results: int = 50,
        start_at: int = 0,
        format_values: bool = True,
    ) -> Dict[str, Any]:
        """
        Get all issues from a specific project

        Args:
            project_key: Project key (e.g., "PROJ")
            properties: List of properties to extract
            max_results: Maximum number of results
            start_at: Starting index
            format_values: Whether to format complex values for readability

        Returns:
            Dictionary containing project issues
        """
        jql = f"project = {project_key}"
        return self.search_issues(jql, properties, max_results, start_at, format_values)

    def get_custom_fields(self) -> List[Dict[str, Any]]:
        """
        Get all custom fields

        Returns:
            List of custom field dictionaries
        """
        try:
            response = self._make_request("GET", "/field")
            # Filter only custom fields
            custom_fields = [field for field in response if field.get("custom")]
            return custom_fields
        except Exception as e:
            self.logger.error(f"Failed to get custom fields: {e}")
            raise

    def create_issue(
        self,
        project_key: str,
        issue_type: str,
        summary: str,
        description: str = None,
        **fields,
    ) -> Dict[str, Any]:
        """
        Create a new issue

        Args:
            project_key: Project key
            issue_type: Issue type name
            summary: Issue summary
            description: Issue description
            **fields: Additional fields

        Returns:
            Created issue data
        """
        try:
            payload = {
                "fields": {
                    "project": {"key": project_key},
                    "issuetype": {"name": issue_type},
                    "summary": summary,
                }
            }

            if description:
                payload["fields"]["description"] = {
                    "type": "doc",
                    "version": 1,
                    "content": [
                        {
                            "type": "paragraph",
                            "content": [{"type": "text", "text": description}],
                        }
                    ],
                }

            # Add additional fields
            payload["fields"].update(fields)

            response = self._make_request("POST", "/issue", json=payload)
            self.logger.info(f"Created issue {response.get('key')}")
            return response

        except Exception as e:
            self.logger.error(f"Failed to create issue: {e}")
            raise

    def update_issue(self, issue_key: str, **fields) -> Dict[str, Any]:
        """
        Update an existing issue

        Args:
            issue_key: Issue key
            **fields: Fields to update

        Returns:
            Updated issue data
        """
        try:
            payload = {"fields": fields}
            self._make_request("PUT", f"/issue/{issue_key}", json=payload)
            self.logger.info(f"Updated issue {issue_key}")
            return self.get_issue(issue_key)
        except Exception as e:
            self.logger.error(f"Failed to update issue {issue_key}: {e}")
            raise

    def add_comment(self, issue_key: str, comment: str) -> Dict[str, Any]:
        """
        Add comment to an issue

        Args:
            issue_key: Issue key
            comment: Comment text

        Returns:
            Comment data
        """
        try:
            payload = {
                "body": {
                    "type": "doc",
                    "version": 1,
                    "content": [
                        {
                            "type": "paragraph",
                            "content": [{"type": "text", "text": comment}],
                        }
                    ],
                }
            }

            response = self._make_request(
                "POST", f"/issue/{issue_key}/comment", json=payload
            )
            self.logger.info(f"Added comment to issue {issue_key}")
            return response
        except Exception as e:
            self.logger.error(f"Failed to add comment to issue {issue_key}: {e}")
            raise

    def get_issue_transitions(self, issue_key: str) -> List[Dict[str, Any]]:
        """
        Get available transitions for an issue

        Args:
            issue_key: Issue key

        Returns:
            List of available transitions
        """
        try:
            response = self._make_request("GET", f"/issue/{issue_key}/transitions")
            return response.get("transitions", [])
        except Exception as e:
            self.logger.error(f"Failed to get transitions for issue {issue_key}: {e}")
            raise

    def transition_issue(self, issue_key: str, transition_id: str) -> bool:
        """
        Transition issue to new status

        Args:
            issue_key: Issue key
            transition_id: Transition ID

        Returns:
            True if successful
        """
        try:
            payload = {"transition": {"id": transition_id}}

            self._make_request("POST", f"/issue/{issue_key}/transitions", json=payload)
            self.logger.info(f"Transitioned issue {issue_key}")
            return True
        except Exception as e:
            self.logger.error(f"Failed to transition issue {issue_key}: {e}")
            return False

    def get_available_fields(self) -> List[Dict[str, Any]]:
        """
        Get all available fields including custom fields

        Returns:
            List of field dictionaries with id, name, and type
        """
        try:
            response = self._make_request("GET", "/field")
            return response
        except Exception as e:
            self.logger.error(f"Failed to get available fields: {e}")
            raise

    def format_issue_for_display(
        self, issue: Dict[str, Any], properties: List[str] = None
    ) -> str:
        """
        Format issue data for human-readable display

        Args:
            issue: Issue data dictionary
            properties: Specific properties to display (None for all)

        Returns:
            Formatted string representation
        """
        lines = []
        lines.append(f"Issue: {issue.get('key', 'N/A')}")
        lines.append("=" * 50)

        if properties:
            for prop in properties:
                value = issue.get(prop)
                if value is not None:
                    # Clean up property name for display
                    display_name = prop.replace("fields.", "").replace("_", " ").title()
                    lines.append(f"{display_name}: {value}")
        else:
            for key, value in issue.items():
                if key != "key":
                    display_name = key.replace("fields.", "").replace("_", " ").title()
                    lines.append(f"{display_name}: {value}")

        return "\n".join(lines)

    def close(self):
        """Close the session"""
        if self.session:
            self.session.close()
            self.logger.info("Jira connector session closed")
