import requests
from typing import Dict, Any, Optional, Union, Callable
import json
import configparser
import os
import argparse
import logging
import pkg_resources
import sys
import time
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

from .. import cli

class APIClient:
    """Client for interacting with the server API."""
    
    def __init__(self, base_url: str = "http://server-url:18080/api/v1", config_file: str = "config.ini", args: Optional[argparse.Namespace] = None):
        """Initialize the API client."""
        self.config = configparser.ConfigParser()
        
        config_paths = [
            os.path.join("configs", config_file)
        ]
        
        if args and hasattr(args, 'env'):
            env_suffix = f"_{args.env}" if args.env != "production" else ""
            env_config_file = f"config{env_suffix}.ini"
            env_config_paths = [
                os.path.join("configs", env_config_file)
            ]
            config_paths = env_config_paths
        
        config_found = False
        self.config_path_used = None
        
        logging.debug(f"Searching for config files in: {config_paths}")
        for path in config_paths:
            path_exists = os.path.exists(path)
            logging.debug(f"Checking config path: {path}, exists: {path_exists}")
            if path_exists:
                if not os.access(path, os.R_OK):
                    logging.warning(f"Config file exists but is not readable: {path}")
                    print(f"⚠️  Warning: Config file {path} exists but cannot be read.")
                    print(f"   Please check file permissions (try: chmod +r {path})")
                    continue
                
                try:
                    self.config.read(path)
                    config_found = True
                    self.config_path_used = os.path.abspath(path)
                    logging.debug(f"Using configuration from: {self.config_path_used}")
                    
                    if 'server' in self.config and 'base_url' in self.config['server']:
                        logging.debug(f"Config contains server.base_url = {self.config['server']['base_url']}")
                        relative_path = os.path.relpath(self.config_path_used, os.getcwd())
                        print(f"ℹ️  Using config file: {relative_path}")
                    else:
                        logging.debug(f"Config does not contain server.base_url")
                        print(f"⚠️  Config file found but missing server.base_url parameter: {self.config_path_used}")
                    
                    break
                except configparser.ParsingError as e:
                    logging.error(f"Error parsing config file {path}: {str(e)}")
                    print(f"⚠️  Warning: Configuration file {path} has syntax errors and could not be loaded.")
                    print(f"   Please check the file format and try again.")
                    continue
                
        if not config_found:
            self.config['server'] = {'base_url': base_url}
            local_config_path = os.path.join("configs", config_file)
            os.makedirs(os.path.dirname(local_config_path), exist_ok=True)
            with open(local_config_path, 'w') as f:
                self.config.write(f)
            self.config_path_used = os.path.abspath(local_config_path)
            logging.warning(f"Configuration file not found. Created default config at {local_config_path}")
            print(f"ℹ️  Created default configuration file at {local_config_path}")
            
        local_mode = args and args.local
        
        env_var_url = None
        if args and hasattr(args, 'env') and args.env == 'staging':
            env_var_url = os.environ.get('SPARK_ANALYZER_STAGING_HISTORY_SERVER_URL')
            if env_var_url:
                logging.debug(f"Found staging-specific URL in environment: {env_var_url}")
        
        # Priority: 1. Command line arg 2. Environment variable 3. Config file 4. Default URL
        if args and hasattr(args, 'server_url') and args.server_url:
            self.base_url = args.server_url
            logging.debug(f"Using server URL from command line: {self.base_url}")
        elif env_var_url:
            self.base_url = env_var_url
            logging.debug(f"Using server URL from environment variable: {self.base_url}")
            logging.debug(f"All environment variables: {dict([(k,v) for k,v in os.environ.items() if 'SPARK_ANALYZER' in k])}")
        elif 'server' in self.config and 'base_url' in self.config['server']:
            config_url = self.config.get('server', 'base_url').strip()
            if config_url and config_url != "http://server-url:18080/api/v1":
                self.base_url = config_url
                logging.debug(f"Using custom URL from config: {self.base_url}")
            else:
                self.base_url = "http://localhost:18080"
                logging.debug(f"Using default local mode URL: {self.base_url}")
        else:
            self.base_url = "http://localhost:18080"
            logging.debug(f"No URL found in any source, using default: {self.base_url}")
        
        if self.base_url.endswith('/'):
            self.base_url = self.base_url[:-1]
            
        self.original_url = self.base_url
        
        logging.info(f"Using Spark History Server URL: {self.base_url}")
        
        if self.base_url.count('/') > 3 and not '/api/v1' in self.base_url:
            logging.info(f"Detected custom path in URL. API endpoints will be appended as {self.base_url}/api/v1/...")
            
        if args and hasattr(args, 'env') and args.env == 'staging':
            env_var = os.environ.get('SPARK_ANALYZER_STAGING_HISTORY_SERVER_URL')
            if env_var:
                logging.debug(f"Environment has SPARK_ANALYZER_STAGING_HISTORY_SERVER_URL={env_var}")
        
        self.session = requests.Session()
        retry_strategy = Retry(
            total=0,
            backoff_factor=0.5,
            status_forcelist=[429, 500, 502, 503, 504],
            allowed_methods=["GET"]
        )
        adapter = HTTPAdapter(max_retries=retry_strategy)
        self.session.mount("http://", adapter)
        self.session.mount("https://", adapter)
        
        self.timeout = 3
        
        self.raw_cookies = None
        if args and args.browser:
            env_suffix = ""
            if hasattr(args, 'env') and args.env:
                env_suffix = f"_{args.env}"
            
            # Only check in configs directory
            cookie_paths = [
                os.path.join("configs", f"raw_cookies{env_suffix}.txt"),
                os.path.join("configs", "raw_cookies.txt")
            ]
            
            cookie_found = False
            file_exists_but_empty = False
            
            for path in cookie_paths:
                if os.path.exists(path):
                    try:
                        with open(path, "r") as f:
                            raw_content = f.read().strip()
                            
                            non_comment_lines = [line for line in raw_content.split('\n') if line.strip() and not line.strip().startswith('#')]
                            
                            if non_comment_lines:
                                self.raw_cookies = '\n'.join(non_comment_lines)
                                cookie_found = True
                                logging.debug(f"Using cookies from: {path}")
                                break
                            else:
                                file_exists_but_empty = True
                    except Exception as e:
                        logging.error(f"Error reading cookie file {path}: {str(e)}")
                        continue
                    
            if not cookie_found:
                if file_exists_but_empty:
                    print("⚠️  Cookie file found but contains no actual cookies: Browser mode authentication may fail.")
                    print("   Edit the file with your browser cookies:")
                    found_path = next((p for p in cookie_paths if os.path.exists(p)), None)
                    if found_path:
                        print(f"   {os.path.abspath(found_path)}")
                else:
                    print("⚠️  Cookie file not found: Browser mode authentication may fail.")
                    print("   To use browser mode, create the cookie file at:")
                    print("   configs/raw_cookies.txt")
                    print("   Then edit the file with your browser cookies")
            else:
                try:
                    self.cookie_dict = {}
                    if "; " in self.raw_cookies:
                        for item in self.raw_cookies.split("; "):
                            if "=" in item:
                                key, value = item.split("=", 1)
                                self.cookie_dict[key.strip()] = value.strip()
                    elif ";" in self.raw_cookies:
                        for item in self.raw_cookies.split(";"):
                            item = item.strip()
                            if "=" in item:
                                key, value = item.split("=", 1)
                                self.cookie_dict[key.strip()] = value.strip()
                    elif "=" in self.raw_cookies:
                        key, value = self.raw_cookies.split("=", 1)
                        self.cookie_dict[key.strip()] = value.strip()
                    
                    if not self.cookie_dict:
                        logging.warning("Failed to parse cookies. Cookie format may be invalid.")
                        print("⚠️  Warning: Failed to parse cookies. Cookie format may be invalid.")
                        print("   Please ensure you've copied the entire Cookie header from your browser's dev tools")
                        print("   For AWS EMR, make sure you're logged in to the EMR console before copying cookies")
                    else:
                        logging.info(f"Successfully parsed {len(self.cookie_dict)} cookies")
                except Exception as e:
                    logging.error(f"Error parsing cookies: {str(e)}")
                    print("⚠️  Warning: Error parsing cookies. Please check the cookie file format.")
                    print("   For AWS EMR, copy the entire Cookie header from the Network tab in dev tools")
        
        self.headers = self.get_headers()
        
        keyword_paths = [
            os.path.join("resources", "hoodie_keywords.json"),
            pkg_resources.resource_filename("spark_analyzer", os.path.join("resources", "hoodie_keywords.json"))
        ]
        
        keywords_found = False
        for path in keyword_paths:
            if os.path.exists(path):
                try:
                    with open(path, "r") as f:
                        self.hoodie_keywords = json.load(f)
                    keywords_found = True
                    break
                except Exception as e:
                    logging.error(f"Error reading keywords file {path}: {str(e)}")
                    continue
                
        if not keywords_found:
            logging.error("Hoodie keywords file not found. Analysis will be incomplete.")
            print("⚠️  Warning: Hoodie keywords file not found. Stage type classification will be limited.")
            self.hoodie_keywords = {"hoodie_identifiers": [], "scan_keywords": [], "transform_keywords": [], "load_keywords": []}
        
    def get_headers(self) -> Dict[str, Any]:
        """Get the headers for the API request."""
        headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
        }
        return headers
    
    def _request_with_retry(self, method: Callable, url: str, **kwargs) -> requests.Response:
        """Make a request with manual retry logic for cases not covered by the session adapter."""
        max_manual_retries = 0
        retry_delay = 1
        
        for retry_count in range(max_manual_retries + 1):
            try:
                response = method(url, **kwargs)
                return response
            except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
                if retry_count < max_manual_retries:
                    logging.debug(f"Request to {url} failed with {type(e).__name__}. Retrying in {retry_delay}s... (Attempt {retry_count + 1}/{max_manual_retries})")
                    time.sleep(retry_delay)
                    retry_delay *= 2
                else:
                    logging.error(f"Request to {url} failed without retries: {str(e)}")
                    raise
            except Exception as e:
                logging.error(f"Request to {url} failed with unexpected error: {str(e)}")
                raise
    
    def _ensure_api_path(self, url: str) -> str:
        """Ensure the URL contains the Spark API path (/api/v1)."""
        if url.endswith('/'):
            url = url[:-1]
        
        logging.debug(f"Normalizing URL: {url}")
            
        if url.endswith('/api/v1'):
            logging.debug(f"URL already has /api/v1 suffix: {url}")
            return url
            
        if '/api/v1/' in url:
            logging.debug(f"URL already contains /api/v1/ within path: {url}")
            return url
            
        final_url = url + "/api/v1"
        logging.debug(f"Final URL with API path: {final_url}")
        return final_url
        
    def _make_request(self, endpoint: str) -> Dict[str, Any]:
        """Make a GET request to the specified endpoint."""
        if not endpoint.startswith('/'):
            endpoint = '/' + endpoint
        
        api_base_url = self._ensure_api_path(self.base_url)
        url = f"{api_base_url}{endpoint}"
        
        logging.debug(f"Constructed full request URL: {url}")
        
        try:
            logging.debug(f"Making request to: {url}")
            
            kwargs = {"timeout": self.timeout}
            
            if hasattr(self, 'cookie_dict') and self.cookie_dict:
                kwargs["cookies"] = self.cookie_dict
            else:
                kwargs["headers"] = self.headers if self.headers else {}
                
            response = self._request_with_retry(self.session.get, url, **kwargs)
            response.raise_for_status()
            
            try:
                return response.json()
            except json.JSONDecodeError as e:
                logging.error(f"Invalid JSON response from {url}: {str(e)}")
                raise
            
        except requests.exceptions.ConnectionError as e:
            if hasattr(self, 'args') and self.args and hasattr(self.args, 'debug') and self.args.debug:
                logging.error(f"Connection error for URL {url}: {e}")
            
            cli.print_error_box(
                "CONNECTION ERROR",
                "Could not connect to the Spark History Server",
                "1. Check that the server is running and accessible\n" +
                "2. Verify your network connection\n" +
                "3. Check your server URL configuration\n" +
                f"4. Try accessing {self.original_url} in your browser\n" +
                "5. If you know the application ID, use --app_id option"
            )
            
            sys.exit(1)
        except requests.exceptions.Timeout as e:
            if hasattr(self, 'args') and self.args and hasattr(self.args, 'debug') and self.args.debug:
                logging.error(f"Request timeout for {endpoint}: {e}")
            
            cli.print_error_box(
                "CONNECTION TIMEOUT",
                f"The request to the Spark History Server timed out after {self.timeout} seconds",
                "1. Try running the command again later\n" +
                "2. The server might be processing a large amount of data\n" +
                "3. Network conditions might be causing delays\n" +
                "4. If you know the application ID, use --app_id option"
            )
            
            sys.exit(1)
        except requests.exceptions.HTTPError as e:
            status_code = e.response.status_code
            
            if status_code == 404:
                logging.error(f"⚠️  Could not connect to Spark History Server at {self.original_url}")
            else:
                logging.error(f"HTTP error {status_code} for {endpoint}: {e}")
            
            if status_code == 401 or status_code == 403:
                title = f"AUTHENTICATION ERROR ({status_code})"
                message = "Server authentication failed"
                help_text = "1. Check your authentication credentials\n" + \
                           "2. Update your browser cookies if using browser mode\n" + \
                           "3. Try logging in again to the Spark History Server in your browser\n" + \
                           ("4. For AWS EMR: ensure your IAM permissions are correct\n" if "amazonaws.com" in self.base_url else "")
            elif status_code == 404:
                title = f"SPARK HISTORY SERVER NOT FOUND"
                message = f"The requested resource was not found: {endpoint}"
                help_text = "1. Verify your Spark History Server URL is correct\n" + \
                           "2. Check that the application ID or stage ID is correct\n" + \
                           "3. Try using --app_id to specify an application directly"
            elif status_code == 429:
                title = f"TOO MANY REQUESTS ({status_code})"
                message = "The server is rate limiting your connections"
                help_text = "1. Wait a few minutes and try again\n" + \
                           "2. Reduce the frequency of your requests"
            elif status_code >= 500:
                title = f"SERVER ERROR ({status_code})"
                message = "The Spark History Server encountered an internal problem"
                help_text = "1. Wait a few minutes and try again\n" + \
                           "2. The server might be overloaded or experiencing issues\n" + \
                           "3. If persistent, contact your Spark admin"
            else:
                title = f"HTTP ERROR ({status_code})"
                message = f"Unexpected HTTP error: {str(e)}"
                help_text = "1. Try again later\n" + \
                           "2. Check your server URL and parameters"
            
            cli.print_error_box(title, message, help_text)
            
            sys.exit(1)
        except json.JSONDecodeError:
            logging.error(f"JSON decode error for {endpoint}")
            
            cli.print_error_box(
                "INVALID SERVER RESPONSE",
                "The server returned a non-JSON response",
                "1. Verify your server URL configuration\n" +
                "2. Make sure you're connecting to the API endpoint, not the web UI\n" +
                "3. Try adding or removing '/api/v1' from your URL\n" +
                f"4. Current URL: {self.original_url}"
            )
            
            sys.exit(1)
    
    def get_applications(self) -> Dict[str, Any]:
        """Get all applications."""
        logging.debug("Fetching applications list")
        try:
            apps = self._make_request("/applications")
            logging.debug(f"Found {len(apps)} applications")
            return apps
        except Exception as e:
            logging.error(f"Failed to get applications: {str(e)}")
            raise

    def get_executor_metrics(self, app_id: str) -> Dict[str, Any]:
        """Get executor metrics for a specific application."""
        logging.debug(f"Fetching executor metrics for application {app_id}")
        try:
            clean_app_id = app_id.strip()
            metrics = self._make_request(f"/applications/{clean_app_id}/executors")
            logging.debug(f"Found {len(metrics)} executor metrics entries")
            return metrics
        except Exception as e:
            logging.error(f"Failed to get executor metrics for {app_id}: {str(e)}")
            raise
    
    def get_all_executor_metrics(self,app_id: str) -> Dict[str, Any]:
        """Get all executor metrics for all applications."""
        logging.debug(f"Fetching all executor metrics for application {app_id}")
        try:
            clean_app_id = app_id.strip()
            metrics = self._make_request(f"/applications/{clean_app_id}/allexecutors")
            logging.debug(f"Found {len(metrics)} total executor metrics entries")
            return metrics
        except Exception as e:
            logging.error(f"Failed to get all executor metrics for {app_id}: {str(e)}")
            raise
    
    def get_application_environment(self, app_id: str) -> Dict[str, Any]:
        """Get the environment for a specific application."""
        logging.debug(f"Fetching environment for application {app_id}")
        try:
            clean_app_id = app_id.strip()
            env = self._make_request(f"/applications/{clean_app_id}/environment")
            logging.debug(f"Successfully retrieved environment for {app_id}")
            return env
        except Exception as e:
            logging.error(f"Failed to get environment for {app_id}: {str(e)}")
            raise
    
    def get_stage_details(self, app_id: str, stage_id: str) -> Dict[str, Any]:
        """Get details for a specific stage."""
        logging.debug(f"Fetching details for stage {stage_id} in application {app_id}")
        try:
            clean_app_id = app_id.strip()
            clean_stage_id = str(stage_id).strip()
            details = self._make_request(f"/applications/{clean_app_id}/stages/{clean_stage_id}?details=false")
            logging.debug(f"Successfully retrieved details for stage {stage_id}")
            return details
        except Exception as e:
            logging.error(f"Failed to get stage details for {app_id}/{stage_id}: {str(e)}")
            raise
    
    def get_stage_details_with_tasks(self, app_id: str, stage_id: str) -> Dict[str, Any]:
        """Get details for a specific stage with tasks."""
        logging.debug(f"Fetching details with tasks for stage {stage_id} in application {app_id}")
        try:
            clean_app_id = app_id.strip()
            clean_stage_id = str(stage_id).strip()
            details = self._make_request(f"/applications/{clean_app_id}/stages/{clean_stage_id}?details=true")
            
            if details and isinstance(details, list) and len(details) > 0:
                stage_attempt = details[0]
                
                if "tasks" not in stage_attempt or not stage_attempt["tasks"]:
                    logging.debug(f"No task data found for stage {stage_id} - this is normal for stages that are queued or just starting")
                    return details
            
            logging.debug(f"Successfully retrieved details with tasks for stage {stage_id}")
            return details
        except Exception as e:
            logging.error(f"Failed to get stage details with tasks for {app_id}/{stage_id}: {str(e)}")
            raise
    
    def get_stages(self, app_id: str) -> Dict[str, Any]:
        """Get all stages for a specific application."""
        logging.debug(f"Fetching all stages for application {app_id}")
        try:
            clean_app_id = app_id.strip()
            stages = self._make_request(f"/applications/{clean_app_id}/stages")
            logging.debug(f"Found {len(stages)} stages for application {app_id}")
            return stages
        except Exception as e:
            logging.error(f"Failed to get stages for {app_id}: {str(e)}")
            raise
    
    def is_indexing_candidate(self, stage):
        """Check if a stage is an RLI (Record Level Indexing) candidate."""
        if isinstance(stage, list):
            if not stage:
                return False
            stage = stage[0]
        
        description = stage.get("description", "")
        if not description:
            return False
            
        for keyword in self.hoodie_keywords.get("rli_keywords", []):
            if keyword.lower() in description.lower():
                return True
                
        return False
    
    def get_stage(self, app_id: str, stage_id: str) -> Dict[str, Any]:
        """Get a specific stage from an application."""
        return self.get_stage_details(app_id, stage_id) 