#!/usr/bin/env python3
"""
Pipeline orchestration for VulnSeeker.
This module coordinates the complete analysis pipeline:
1. Fetch CodeQL databases
2. Run CodeQL queries
3. Classify results with LLM
"""
# Ignore pydantic warnings
import warnings
warnings.filterwarnings("ignore", category=UserWarning, module="pydantic")

import argparse
import sys
from pathlib import Path
from typing import Optional

# Add project root to Python path
PROJECT_ROOT = Path(__file__).parent.parent
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from src.codeql.fetch_repos import fetch_codeql_dbs, build_local_codeql_db
from src.codeql.run_codeql_queries import compile_and_run_codeql_queries
from src.utils.config import get_codeql_path
from src.utils.config_validator import validate_and_exit_on_error
from src.utils.logger import setup_logging, get_logger
from src.utils.exceptions import (
    CodeQLError, CodeQLConfigError, CodeQLExecutionError,
    LLMError, LLMConfigError, LLMApiError,
    VulnSeekerError
)
from src.vulnseeker import IssueAnalyzer

# Initialize logging
setup_logging()
logger = get_logger(__name__)


def _log_exception_cause(e: Exception) -> None:
    """
    Log the cause of an exception if available and not already included in the exception message.
    Checks both e.cause (if set via constructor) and e.__cause__ (if set via 'from e').
    """
    cause = getattr(e, 'cause', None) or getattr(e, '__cause__', None)
    if cause:
        # Only log cause if it's not already included in the exception message
        cause_str = str(cause)
        error_str = str(e)
        if cause_str not in error_str:
            logger.error("   Cause: %s", cause)


def step1_fetch_codeql_dbs(lang: str, threads: int, repo: str, force: bool = False) -> str:
    """
    Step 1: Fetch CodeQL databases from GitHub.

    Args:
        lang: Programming language code.
        threads: Number of threads for download operations.
        repo: Repository name (e.g., "redis/redis").

    Returns:
        str: Path to the directory containing downloaded databases.

    Raises:
        CodeQLConfigError: If configuration is invalid (e.g., missing GitHub token).
        CodeQLError: If database download or extraction fails.
    """
    logger.info("\nStep 1: Fetching CodeQL Databases")
    logger.info("-" * 60)
    logger.info("Fetching database for: %s", repo)

    try:
        dbs_dir = fetch_codeql_dbs(lang=lang, threads=threads, repo_name=repo, force=force)
        if not dbs_dir:
            raise CodeQLError(f"No CodeQL databases were downloaded/found for {repo}")
        return dbs_dir
    except CodeQLConfigError as e:
        logger.error("[-] Step 1: Configuration error while fetching CodeQL databases: %s", e)
        _log_exception_cause(e)
        logger.error("   Please check your GitHub token and permissions.")
        sys.exit(1)
    except CodeQLError as e:
        logger.error("[-] Step 1: Failed to fetch CodeQL databases: %s", e)
        _log_exception_cause(e)
        logger.error("   Please check file permissions, disk space, and GitHub API access.")
        sys.exit(1)


def step1_build_local_db(lang: str, local_src_path: str, threads: int, force: bool = False) -> str:
    """
    Step 1: Build a CodeQL database from local source code.

    Args:
        lang: Programming language code.
        local_src_path: Path to the local source code directory.
        threads: Number of threads for database creation.
        force: If True, re-build even if database already exists.

    Returns:
        str: Path to the directory containing the built database.

    Raises:
        CodeQLConfigError: If CodeQL executable not found or source path invalid.
        CodeQLExecutionError: If database creation fails.
        CodeQLError: If output directory creation fails.
    """
    logger.info("\nStep 1: Building CodeQL Database from Local Source")
    logger.info("-" * 60)
    logger.info("Source path: %s", local_src_path)

    try:
        dbs_dir = build_local_codeql_db(
            source_path=local_src_path,
            lang=lang,
            threads=threads,
            force=force
        )
        return dbs_dir
    except CodeQLConfigError as e:
        logger.error("[-] Step 1: Configuration error while building CodeQL database: %s", e)
        _log_exception_cause(e)
        logger.error("   Please check your CODEQL_PATH and source path.")
        sys.exit(1)
    except CodeQLExecutionError as e:
        logger.error("[-] Step 1: Failed to build CodeQL database: %s", e)
        _log_exception_cause(e)
        logger.error("   Please check CodeQL installation and source code compatibility.")
        sys.exit(1)
    except CodeQLError as e:
        logger.error("[-] Step 1: Failed to build CodeQL database: %s", e)
        _log_exception_cause(e)
        logger.error("   Please check file permissions and disk space.")
        sys.exit(1)


def step2_run_codeql_queries(dbs_dir: str, lang: str, threads: int) -> None:
    """
    Step 2: Run CodeQL queries on the downloaded databases.
    
    Args:
        dbs_dir: Path to the directory containing CodeQL databases.
        lang: Programming language code.
        threads: Number of threads for query execution.
    
    Raises:
        CodeQLConfigError: If CodeQL path configuration is invalid.
        CodeQLExecutionError: If query execution fails.
        CodeQLError: If other CodeQL-related errors occur (e.g., database access issues).
    """
    logger.info("\nStep 2: Running CodeQL Queries")
    logger.info("-" * 60)
    
    try:
        compile_and_run_codeql_queries(
            codeql_bin=get_codeql_path(),
            lang=lang,
            threads=threads,
            timeout=300,
            dbs_dir=dbs_dir
        )
    except CodeQLConfigError as e:
        logger.error("[-] Step 2: Configuration error while running CodeQL queries: %s", e)
        _log_exception_cause(e)
        logger.error("   Please check your CODEQL_PATH configuration.")
        sys.exit(1)
    except CodeQLExecutionError as e:
        logger.error("[-] Step 2: Failed to execute CodeQL queries: %s", e)
        _log_exception_cause(e)
        logger.error("   Please check your CodeQL installation and database files.")
        sys.exit(1)
    except CodeQLError as e:
        logger.error("[-] Step 2: CodeQL error while running queries: %s", e)
        _log_exception_cause(e)
        logger.error("   Please check your CodeQL database files and query syntax.")
        sys.exit(1)
    

def step3_classify_results_with_llm(dbs_dir: str, lang: str) -> None:
    """
    Step 3: Classify CodeQL results using LLM analysis.
    
    Args:
        dbs_dir: Path to the directory containing CodeQL databases.
        lang: Programming language code (can be comma-separated list).
    
    Raises:
        LLMConfigError: If LLM configuration is invalid (e.g., missing API credentials).
        LLMApiError: If LLM API call fails (e.g., network issues, rate limits).
        LLMError: If other LLM-related errors occur.
        CodeQLError: If reading CodeQL database files fails (YAML, ZIP, CSV).
        VulnSeekerError: If saving analysis results to disk fails.
    """
    logger.info("\nStep 3: Classifying Results with LLM")
    logger.info("-" * 60)
    
    try:
        lang_list = [l.strip() for l in lang.split(",")]
        for current_lang in lang_list:
            logger.info(f"Classifying results for language: {current_lang}")
            analyzer = IssueAnalyzer(lang=current_lang)
            analyzer.run(dbs_dir)
    except LLMConfigError as e:
        logger.error("[-] Step 3: LLM configuration error: %s", e)
        _log_exception_cause(e)
        logger.error("   Please check your LLM configuration and API credentials in .env file.")
        sys.exit(1)
    except LLMApiError as e:
        logger.error("[-] Step 3: LLM API error: %s", e)
        _log_exception_cause(e)
        logger.error("   Please check your API key, network connection, and rate limits.")
        sys.exit(1)
    except LLMError as e:
        logger.error("[-] Step 3: LLM error: %s", e)
        _log_exception_cause(e)
        logger.error("   Please check your LLM provider settings and API status.")
        sys.exit(1)
    except CodeQLError as e:
        logger.error("[-] Step 3: CodeQL error while reading database files: %s", e)
        _log_exception_cause(e)
        logger.error("   This step reads CodeQL database files (YAML, ZIP, CSV) to prepare data for LLM analysis.")
        logger.error("   Please check your CodeQL databases and files are accessible.")
        sys.exit(1)
    except VulnSeekerError as e:
        logger.error("[-] Step 3: File system error while saving results: %s", e)
        _log_exception_cause(e)
        logger.error("   This step writes analysis results to disk and creates output directories.")
        logger.error("   Please check file permissions and disk space.")
        sys.exit(1)


def main_analyze() -> None:
    """
    CLI entry point for the complete analysis pipeline.
    
    Expected usage: 
        vulnseeker <org/repo> [--force]           # Fetch from GitHub
        vulnseeker --local <path/to/db>           # Use local CodeQL database
    """
    parser = argparse.ArgumentParser(
        prog="vulnseeker",
        description="VulnSeeker - Automated CodeQL Analysis with LLM Classification"
    )
    parser.add_argument("repo", nargs="?", help="GitHub repository in 'org/repo' format")
    parser.add_argument("--force", "-f", action="store_true", help="Re-download even if database exists")
    parser.add_argument("--local", "-l", metavar="PATH", help="Path to local CodeQL database (skips GitHub fetch)")
    parser.add_argument("--local-src", metavar="PATH", help="Path to local source code to build CodeQL database from")
    
    args = parser.parse_args()

    # Validate arguments
    if args.local_src:
        # Build from local source mode
        local_src_path = Path(args.local_src)
        if not local_src_path.exists():
            parser.error(f"Source path does not exist: {args.local_src}")
        analyze_pipeline(repo=None, local_src_path=str(local_src_path), force=args.force)
    elif args.local:
        # Local database mode
        local_path = Path(args.local)
        if not local_path.exists():
            parser.error(f"Local database path does not exist: {args.local}")
        analyze_pipeline(repo=None, local_db_path=str(local_path), force=args.force)
    elif args.repo:
        # GitHub fetch mode
        if "/" not in args.repo:
            parser.error("Repository must be in format 'org/repo'")
        analyze_pipeline(repo=args.repo, force=args.force)
    else:
        parser.error("Either provide a repository (org/repo) or use --local <path>")


def analyze_pipeline(
    repo: Optional[str] = None,
    lang: str = "c",
    threads: int = 16,
    force: bool = False,
    local_db_path: Optional[str] = None,
    local_src_path: Optional[str] = None
) -> None:
    """
    Run the complete VulnSeeker pipeline: fetch, analyze, classify.

    Args:
        repo: GitHub repository name (e.g., "redis/redis"). Required if local_db_path and
              local_src_path are not provided.
        lang: Programming language code. Defaults to "c".
        threads: Number of threads for CodeQL operations. Defaults to 16.
        force: If True, re-download/rebuild even if database exists. Defaults to False.
        local_db_path: Path to local CodeQL database. If provided, skips GitHub fetch.
        local_src_path: Path to local source code. If provided, builds CodeQL database
                        from source before analysis.

    Note:
        This function catches and handles all exceptions internally, logging errors
        and exiting with code 1 on failure. It does not raise exceptions.
    """
    logger.info("Starting VulnSeeker Analysis Pipeline")
    logger.info("=" * 60)

    # Validate configuration before starting
    try:
        validate_and_exit_on_error()
    except (CodeQLConfigError, LLMConfigError, VulnSeekerError) as e:
        message = f"""
[-] Configuration Validation Failed
============================================================
{str(e)}
============================================================
Please fix the configuration errors above and try again.
See README.md for configuration reference.
"""
        logger.error(message)
        _log_exception_cause(e)
        sys.exit(1)

    # Step 1: Fetch / Build / Use local database
    if local_src_path:
        # Build CodeQL database from local source code
        dbs_dir = step1_build_local_db(lang, local_src_path, threads, force)
    elif local_db_path:
        # Use existing local database
        logger.info("\nStep 1: Using Local CodeQL Database")
        logger.info("-" * 60)
        logger.info("Database path: %s", local_db_path)
        dbs_dir = local_db_path
    else:
        # Fetch from GitHub
        dbs_dir = step1_fetch_codeql_dbs(lang, threads, repo, force)
    
    # Step 2: Run CodeQL queries
    step2_run_codeql_queries(dbs_dir, lang, threads)
    
    # Step 3: Classify results with LLM
    step3_classify_results_with_llm(dbs_dir, lang)


def main_validate() -> None:
    """
    CLI entry point to validate configuration.
    
    Expected usage: vulnseeker-validate
    """
    from src.utils.config_validator import validate_all_config
    
    is_valid, errors = validate_all_config()
    
    if is_valid:
        logger.info("[+] All configurations are valid!")
    else:
        for error in errors:
            logger.error(error)
        sys.exit(1)


def main_example() -> None:
    """
    CLI entry point to run the example pipeline.
    
    Expected usage: vulnseeker-example
    """
    from examples.example import main as example_main
    example_main()


if __name__ == '__main__':
    main_analyze()
