import logging
from dataclasses import dataclass
from typing import Optional, Union

import requests
from jira import JIRA, JIRAError
from jira.resources import GreenHopperResource
from requests.auth import AuthBase

from jf_ingest import logging_helper
from jf_ingest.config import JiraAuthConfig, JiraAuthMethod, JiraDownloadConfig
from jf_ingest.constants import Constants
from jf_ingest.jf_jira.exceptions import AtlassianConnectException
from jf_ingest.utils import retry_for_status

logger = logging.getLogger(__name__)


class JiraAuthenticationException(Exception):
    def __init__(self, *args, original_exception: Optional[Exception] = None):
        self.original_exception = original_exception
        super().__init__(*args)


# HACK(asm,2024-05-10): We are seeing questionable HTTP 401 responses from Jira that seem to be
# related to cookie-based authentication validation. Specifically, we believe that requests made
# using a bearer token are creating an expiring session on the server side, and it is causing Jira
# to (incorrectly) challenge us to re-authenticate based on this expired session. To avoid this, we
# are customizing the authentication method so that we never send cookies when we are using bearer
# token authentication. If this works, this should be upstreamed.
class TokenAuth(AuthBase):
    def __init__(self, token: str):
        self._token = token

    def __call__(self, r: requests.PreparedRequest) -> requests.PreparedRequest:
        # never send cookies for token auth
        r.headers["Cookie"] = ''
        r.headers["authorization"] = f"Bearer {self._token}"
        return r


def get_jira_connection(
    config: Union[JiraAuthConfig, JiraDownloadConfig],
    auth_method: JiraAuthMethod = JiraAuthMethod.BasicAuth,
    max_retries=3,
    use_jql_enhanced_search: bool = False,
) -> JIRA:
    """Low-level function to create a single JIRA connection object.

    WARNING: Most callers should use ``create_jira_connection`` or
    ``create_jira_connections`` instead. Those higher-level factories
    automatically handle JQL Enhanced Search API detection via feature
    flags and live endpoint testing. Use this function directly only
    when you need fine-grained control over connection parameters
    (e.g. custom max_retries) or when you already know the correct
    ``use_jql_enhanced_search`` value.

    Args:
        config (JiraAuthConfig | JiraDownloadConfig): A JIRA Configuration Object that contains authentication information
        auth_method (JiraAuthMethod, optional): The Authentication method used. BasicAuth and AtlassianConnect are currently supported. Defaults to JiraAuthMethod.BasicAuth.
        max_retries (int, optional): The retry limit used by the JiraConnection.ResilientSession object. Defaults to 3.
        use_jql_enhanced_search (bool, optional): Whether to configure connection for JQL Enhanced Search API. When True, uses REST API v3 for /search/jql endpoints. When False, uses REST API v2 for backward compatibility. Defaults to False.

    Raises:
        Several Errors can be raised by this class. Please ensure your config is properly set up

    Returns:
        JIRA: A JIRA connection object
    """
    should_ssl_verify = not config.bypass_ssl_verification

    # This is the base Jira Connection KWARGs. For different
    # authentication methods, we will need to add specific fields
    jira_conn_kwargs = {
        "timeout": 600,
        "server": config.url,
        "max_retries": max_retries,
        "options": {
            "agile_rest_path": GreenHopperResource.AGILE_BASE_REST_PATH,
            "verify": should_ssl_verify,
            "headers": {
                "Cache-Control": "no-cache",
                "Content-Type": "application/json",
                "Accept": "application/json,*/*;q=0.9",
                "X-Atlassian-Token": "no-check",
            },
        },
    }

    # Determine REST API version based on intended usage
    if use_jql_enhanced_search:
        jira_conn_kwargs['options']['rest_api_version'] = '3'

    using_token_auth = False

    if auth_method.value == JiraAuthMethod.AtlassianConnect.value:
        if not config.connect_app_active:
            raise RuntimeError(
                f'Atlassian connect integration for {config.company_slug} is disabled. Check "connect_app_active" flag state.'
            )

        # The customer has installed our Atlassian Connect app; we authenticate to the Jira
        # API using the JWT attributes
        shared_secret = config.jwt_attributes.get("sharedSecret")
        client_key = config.jwt_attributes.get("clientKey")
        key = config.jwt_attributes.get("key")

        if not (shared_secret and client_key and key):
            raise RuntimeError(
                f"Atlassian connect integration for {config.company_slug} is misconfigured"
            )

        logger.debug(f"Authenticating to Jira API at {config.url} using JWT attributes")
        jira_conn_kwargs['jwt'] = {
            "secret": shared_secret,
            "payload": {"iss": key, "aud": client_key},
        }
        jira_conn: JIRA = retry_for_status(
            JIRA, max_retries_for_retry_for_status=3, **jira_conn_kwargs
        )

    elif auth_method.value == JiraAuthMethod.BasicAuth.value:
        # Attempt to log a helpful warning if a customer has a Jira User provided but NOT a jira password provided
        if config.user and not config.password:
            optional_bearer_token_message = (
                f'A Jira Bearer token was being detected as being provided, so we will attempt to authenticate with that instead. '
                'If you would like to authenticate with only the Bear token please remove the Jira User field provided, as it is not necessary.'
            )
            logger.warning(
                f'Jira Authentication has detected that a Jira User value has been provided, '
                'but that Jira Password value was left blank. '
                'If you would like to authenticate using a User/Password scheme, please provide the username and password. '
                f'{optional_bearer_token_message if config.personal_access_token else ""}'
            )

        # Attempt user/password auth only if we have both a user and a password
        if config.user and config.password:
            logger.debug(
                f"Authenticating to Jira API at {config.url} "
                f"using the username and password secrets for {config.user} of company {config.company_slug}"
            )
            # Add in basic auth info
            jira_conn_kwargs['basic_auth'] = (config.user, config.password)
        elif config.personal_access_token:
            logger.debug(
                f"Authenticating to Jira API at {config.url} "
                f"using the personal_access_token secret for {config.company_slug}"
            )
            # Add in personal access token
            jira_conn_kwargs['token_auth'] = config.personal_access_token

            using_token_auth = True
        else:
            raise RuntimeError(
                f"No valid basic authentication mechanism for {config.url} - need a username/password combo or a personal access token"
            )

        while True:
            try:
                jira_conn = retry_for_status(
                    JIRA, max_retries_for_retry_for_status=3, **jira_conn_kwargs
                )
                break
            except JIRAError as e:  # catch generic error raised from JIRA
                if hasattr(e, "status_code") and e.status_code in (401, 403):
                    raise JiraAuthenticationException(
                        f"Jira authentication (HTTP ERROR CODE {e.status_code}) failed for {config.url} using {config.user} and the jira_password secret for {config.company_slug}",
                        original_exception=e,
                    )
                else:
                    raise
    else:
        raise RuntimeError(
            f"No valid authentication mechanism for {config.url} (Auth Method: {auth_method})."
        )

    # set user-agent
    jira_conn._session.headers["User-Agent"] = (
        f'{config.user_agent} ({jira_conn._session.headers["User-Agent"]})'
    )

    # HACK(asm,2024-05-10): See comment above `TokenAuth` class definition - overriding
    # authentication here to force no cookies.
    if using_token_auth and config.personal_access_token:
        jira_conn._session.auth = TokenAuth(config.personal_access_token)

    return jira_conn


def _test_enhanced_search_endpoint(jira_config: JiraDownloadConfig) -> bool:
    """Test if the enhanced /search/jql (v3) endpoint is reachable and working.

    Args:
        jira_config: The Jira configuration to use for creating a test connection

    Returns:
        bool: True if the endpoint returns a successful response, False otherwise
    """
    try:
        test_connection = get_jira_connection(config=jira_config, use_jql_enhanced_search=True)
    except Exception as e:
        logger.warning(f"Failed to create v3 test connection: {e}")
        return False

    try:
        payload = {
            'jql': 'updatedDate >= -1d order by id',
            'fields': ['id'],
            'maxResults': 1,
        }
        url = test_connection._get_url('search/jql')
        response = test_connection._session.post(url=url, json=payload)

        if 200 <= response.status_code < 300:
            return True

        logger.debug(f"Enhanced search endpoint /search/jql returned status {response.status_code}")
    except Exception as e:
        logger.debug(f"Failed to test /search/jql endpoint: {e}")

    return False


def _test_legacy_search_endpoint(jira_config: JiraDownloadConfig) -> bool:
    """Test if the legacy /search (v2) endpoint is reachable and working.

    Args:
        jira_config: The Jira configuration to use for creating a test connection

    Returns:
        bool: True if the endpoint returns a successful response, False otherwise
    """
    try:
        test_connection = get_jira_connection(config=jira_config, use_jql_enhanced_search=False)
    except Exception as e:
        logger.warning(f"Failed to create v2 test connection: {e}")
        return False

    try:
        payload = {
            'jql': 'updatedDate >= -1d order by id',
            'fields': ['id'],
            'maxResults': 1,
        }
        url = test_connection._get_url('search')
        response = test_connection._session.post(url=url, json=payload)

        if 200 <= response.status_code < 300:
            return True

        logger.debug(f"Legacy search endpoint /search returned status {response.status_code}")
    except Exception as e:
        logger.debug(f"Failed to test /search endpoint: {e}")

    return False


def determine_api_search_version(
    jira_config: JiraDownloadConfig,
    jql_enhanced_search_enabled: bool = False,
    force_legacy_api: bool = False,
) -> bool:
    """Dynamically determine which Jira search API to use by testing endpoint availability.

    Always tests the preferred endpoint with a live API call. If the preferred
    endpoint is unavailable, automatically falls back to the alternative.

    The enhanced /search/jql endpoint (v3 REST API):
    - Uses nextPageToken instead of startAt for pagination
    - Has a 5,000 issue limit (vs 10,000 for legacy /search)
    - May not be available on older Jira instances

    The legacy /search endpoint (v2 REST API):
    - Uses startAt for pagination
    - Is being deprecated on newer Jira Cloud instances

    Behavior by feature flags:
    - force_legacy_api=True: Always returns False (legacy), no endpoint testing. Use as an
      emergency escape hatch.
    - jql_enhanced_search_enabled=True: Tests enhanced endpoint first; if it fails, falls
      back to legacy.
    - jql_enhanced_search_enabled=False (default): Tests legacy endpoint first; if it fails,
      tests enhanced endpoint and uses it if available.

    Args:
        jira_config: The Jira configuration to use for creating the connection
        jql_enhanced_search_enabled: Whether the enhanced API is preferred via feature flag
        force_legacy_api: Whether to force use of legacy API via feature flag (escape hatch)

    Returns:
        bool: True to use enhanced /search/jql (v3), False to use legacy /search (v2)
    """
    # Escape hatch: force legacy with no testing
    if force_legacy_api:
        logger.info("Using Legacy API for Jira operations (forced via feature flag)")
        return False

    if jql_enhanced_search_enabled:
        # Prefer enhanced: test enhanced first, fall back to legacy
        if _test_enhanced_search_endpoint(jira_config):
            logger.info("Using JQL Enhanced Search API (/search/jql) for Jira operations")
            return True
        logger.info(
            "JQL Enhanced Search API preferred but unavailable, falling back to legacy /search API"
        )
        return False
    else:
        # Prefer legacy: test legacy first, fall back to enhanced if legacy is broken
        if _test_legacy_search_endpoint(jira_config):
            logger.info("Using Legacy API (/search) for Jira operations")
            return False
        logger.warning(
            "Legacy /search API unavailable, testing enhanced /search/jql API as fallback"
        )
        if _test_enhanced_search_endpoint(jira_config):
            logger.info(
                "Automatically switching to JQL Enhanced Search API (/search/jql) "
                "because legacy /search API is unavailable"
            )
            return True
        # Both endpoints failed -- default to legacy and let the caller deal with errors
        logger.warning(
            "Both search API endpoints had issues during testing, defaulting to legacy /search API"
        )
        return False


# Keep the old name as an alias for backward compatibility
is_jql_enhanced_search_available = determine_api_search_version


@dataclass
class JiraConnectionBundle:
    """
    Holds all Jira connection objects and related configuration produced
    by the standardized connection factory.
    """

    # The primary connection to use for most API calls (BasicAuth or AtlassianConnect based on feature flags)
    primary_connection: JIRA

    # The BasicAuth connection. Always available regardless of which auth is primary
    basic_connection: JIRA

    # An AtlassianConnect connection, if available. May be None
    atlas_connect_connection: Optional[JIRA]

    # Whether the primary_connection is using AtlassianConnect auth
    using_connect_as_primary_auth: bool

    # Whether the JQL Enhanced Search API (v3 /search/jql) is available and should be used
    use_jql_enhanced_search: bool


def create_jira_connection(
    jira_config: JiraDownloadConfig,
    auth_method: JiraAuthMethod = JiraAuthMethod.BasicAuth,
) -> tuple[JIRA, bool]:
    """Create a single Jira connection with automatic enhanced JQL search detection.

    **This is the recommended default** for creating a Jira connection when only
    a single connection is needed (e.g. validation, one-off API calls). It
    automatically detects whether the JQL Enhanced Search API (v3 /search/jql)
    is available by consulting feature flags and performing a live endpoint
    test, then configures the connection accordingly.

    Prefer this over ``get_jira_connection`` unless you need fine-grained
    control over connection parameters such as ``max_retries`` or you already
    know the correct ``use_jql_enhanced_search`` value.

    For the main download flow where both BasicAuth and AtlassianConnect
    connections are needed, use ``create_jira_connections`` instead.

    Args:
        jira_config: Jira download configuration containing auth info and feature flags
        auth_method: The authentication method to use. Defaults to BasicAuth.

    Returns:
        tuple of (JIRA connection, use_jql_enhanced_search bool)
    """
    use_enhanced = determine_api_search_version(
        jira_config=jira_config,
        jql_enhanced_search_enabled=jira_config.feature_flags.get(
            Constants.JQL_ENHANCED_SEARCH_ENABLED, False
        ),
        force_legacy_api=jira_config.feature_flags.get(Constants.FORCE_LEGACY_API, False),
    )
    conn = get_jira_connection(
        config=jira_config,
        auth_method=auth_method,
        use_jql_enhanced_search=use_enhanced,
    )
    return conn, use_enhanced


def create_jira_connections(jira_config: JiraDownloadConfig) -> JiraConnectionBundle:
    """Create all Jira connections with automatic enhanced JQL search detection
    and AtlassianConnect fallback logic.

    **This is the recommended default** for the main download flow where both
    BasicAuth and AtlassianConnect connections are needed. It encapsulates:

    1. Dynamic detection of JQL Enhanced Search (v3) availability
    2. BasicAuth connection creation
    3. Optional AtlassianConnect connection creation
    4. Connect-or-fallback logic based on the lusca feature flag

    For simpler use cases that only need a single connection (e.g. validation),
    use ``create_jira_connection`` instead.

    Args:
        jira_config: Jira download configuration containing auth info and feature flags

    Returns:
        JiraConnectionBundle with all connections and configuration
    """
    # 1. Determine search API version (extracted from feature flags)
    use_jql_enhanced_search = determine_api_search_version(
        jira_config=jira_config,
        jql_enhanced_search_enabled=jira_config.feature_flags.get(
            Constants.JQL_ENHANCED_SEARCH_ENABLED, False
        ),
        force_legacy_api=jira_config.feature_flags.get(Constants.FORCE_LEGACY_API, False),
    )

    # 2. Create BasicAuth connection
    jira_basic_connection = get_jira_connection(
        config=jira_config,
        auth_method=JiraAuthMethod.BasicAuth,
        use_jql_enhanced_search=use_jql_enhanced_search,
    )

    # 3. Create optional AtlassianConnect connection
    jira_atlas_connect_connection = (
        get_jira_connection(
            config=jira_config,
            auth_method=JiraAuthMethod.AtlassianConnect,
            use_jql_enhanced_search=use_jql_enhanced_search,
        )
        if JiraAuthMethod.AtlassianConnect in jira_config.available_auth_methods
        else None
    )

    # 4. Connect-or-fallback logic
    # There is an ongoing effort to cut all things over to Atlassian Connect only,
    # but it is a piecewise migration for now.
    # OJ-29745
    jira_connect_or_fallback_connection = jira_basic_connection
    using_connect_as_primary_auth = False

    if jira_config.feature_flags.get("lusca-auth-always-use-connect-for-atlassian-apis-Q423"):
        logging_helper.send_to_agent_log_file("Will use connect for most API calls")

        if not isinstance(jira_atlas_connect_connection, JIRA):
            raise AtlassianConnectException(
                'Atlassian Connect Connection is not a JIRA object, unable to proceed'
            )

        jira_connect_or_fallback_connection = jira_atlas_connect_connection
        using_connect_as_primary_auth = True
    else:
        logging_helper.send_to_agent_log_file("Will use basic auth for most API calls")

    return JiraConnectionBundle(
        primary_connection=jira_connect_or_fallback_connection,
        basic_connection=jira_basic_connection,
        atlas_connect_connection=jira_atlas_connect_connection,
        using_connect_as_primary_auth=using_connect_as_primary_auth,
        use_jql_enhanced_search=use_jql_enhanced_search,
    )
