use anyhow::Result;
use chrono::Utc;
use colored::*;
use git2::{build::RepoBuilder, FetchOptions, RemoteCallbacks};
use indicatif::{ProgressBar, ProgressStyle};
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Instant;
use tempfile::TempDir;
use uuid::Uuid;
use walkdir::WalkDir;

use crate::analyzers::Analyzer;
use crate::config::AppConfig;
use crate::parsers::{get_parser_for_file, parse_file};
use crate::types::{
    FileResult, ImpactType, Language, ScanResults, ScanSummary, Vulnerability,
};

/// Main scanner that coordinates file discovery and analysis
pub struct Scanner {
    config: AppConfig,
    analyzer: Analyzer,
}

impl Scanner {
    pub fn new(config: AppConfig) -> Self {
        let analyzer = Analyzer::new(
            config.scan.min_severity,
            config.scan.analyze_db_impact,
        );

        Self { config, analyzer }
    }

    /// Scan a path (file or directory) for vulnerabilities (parallel)
    pub async fn scan(&self, path: &Path) -> Result<ScanResults> {
        self.scan_with_options(path, true).await
    }

    /// Scan with optional parallelism control
    pub async fn scan_with_options(&self, path: &Path, parallel: bool) -> Result<ScanResults> {
        // Use scan_id from config when set (e.g. KYBERCHECK_SCAN_ID from dashboard-triggered workflow)
        let scan_id = self
            .config
            .api
            .scan_id
            .as_ref()
            .and_then(|s| Uuid::parse_str(s).ok())
            .unwrap_or_else(Uuid::new_v4);
        let started_at = Utc::now();

        // Collect files to scan
        let files = self.collect_files(path)?;
        let total_files = files.len();

        let num_threads = rayon::current_num_threads();
        println!(
            "{} {} files to scan {}",
            "Found".blue(),
            total_files.to_string().cyan(),
            if parallel {
                format!("(using {} threads)", num_threads).dimmed().to_string()
            } else {
                "(sequential)".dimmed().to_string()
            }
        );

        // Set up progress bar
        let pb = ProgressBar::new(total_files as u64);
        pb.set_style(
            ProgressStyle::default_bar()
                .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
                .unwrap()
                .progress_chars("#>-"),
        );

        let progress_counter = Arc::new(AtomicUsize::new(0));
        let pb_clone = pb.clone();

        // Scan files in parallel or sequential based on option
        let results: Vec<Result<FileResult>> = if parallel {
            files
                .par_iter()
                .map(|file_path| {
                    let result = self.scan_file_sync(file_path);
                    let count = progress_counter.fetch_add(1, Ordering::Relaxed);
                    pb_clone.set_position((count + 1) as u64);
                    result
                })
                .collect()
        } else {
            files
                .iter()
                .map(|file_path| {
                    let result = self.scan_file_sync(file_path);
                    pb.inc(1);
                    result
                })
                .collect()
        };

        pb.finish_with_message("Scan complete");

        // Collect results
        let mut all_vulnerabilities: Vec<Vulnerability> = Vec::new();
        let mut file_results: Vec<FileResult> = Vec::new();
        let mut total_lines = 0usize;

        for result in results {
            match result {
                Ok(file_result) => {
                    total_lines += file_result.lines_scanned;
                    all_vulnerabilities.extend(file_result.vulnerabilities.clone());
                    file_results.push(file_result);
                }
                Err(e) => {
                    tracing::debug!("Error scanning file: {}", e);
                }
            }
        }

        // Check for impacts
        let has_db_impact = all_vulnerabilities
            .iter()
            .any(|v| v.migration_impacts.iter().any(|i| matches!(i.impact_type, ImpactType::DatabaseSchema)));

        let has_api_impact = all_vulnerabilities
            .iter()
            .any(|v| v.migration_impacts.iter().any(|i| matches!(i.impact_type, ImpactType::ApiPayload)));

        // Build summary
        let summary = ScanSummary::from_vulnerabilities(&all_vulnerabilities);

        let completed_at = Utc::now();

        Ok(ScanResults {
            vulnerability_count: all_vulnerabilities.len(),
            scan_id,
            repo_id: self.config.repo.id.clone(),
            started_at,
            completed_at,
            scanned_path: path.to_path_buf(),
            files_scanned: total_files,
            lines_scanned: total_lines,
            vulnerabilities: all_vulnerabilities,
            file_results,
            has_db_impact,
            has_api_impact,
            summary,
        })
    }

    /// Scan a single file (synchronous for parallel processing)
    fn scan_file_sync(&self, path: &Path) -> Result<FileResult> {
        let start = Instant::now();

        // Read file content synchronously
        let content = std::fs::read_to_string(path)?;
        let lines_scanned = content.lines().count();

        // Check file size
        if content.len() > self.config.scan.max_file_size_bytes {
            anyhow::bail!("File too large: {:?}", path);
        }

        // Get language
        let language = path
            .extension()
            .and_then(|e| e.to_str())
            .map(Language::from_extension)
            .unwrap_or(Language::Unknown);

        if language == Language::Unknown {
            return Ok(FileResult {
                path: path.to_path_buf(),
                language,
                vulnerabilities: vec![],
                lines_scanned,
                scan_duration_ms: start.elapsed().as_millis() as u64,
            });
        }

        // Parse and analyze
        let vulnerabilities = match parse_file(path, &content) {
            Ok(parsed) => self.analyzer.analyze(&parsed, path)?,
            Err(_) => {
                // Fall back to pattern-only analysis if parsing fails
                let pattern_matcher = crate::analyzers::PatternMatcher::new();
                pattern_matcher.analyze(&content, path, language, &[])?
            }
        };

        Ok(FileResult {
            path: path.to_path_buf(),
            language,
            vulnerabilities,
            lines_scanned,
            scan_duration_ms: start.elapsed().as_millis() as u64,
        })
    }

    /// Collect all files to scan from a path
    fn collect_files(&self, path: &Path) -> Result<Vec<PathBuf>> {
        let mut files = Vec::new();

        if path.is_file() {
            // Single file
            if self.should_scan_file(path) {
                files.push(path.to_path_buf());
            }
        } else if path.is_dir() {
            // Directory walk
            for entry in WalkDir::new(path)
                .follow_links(self.config.scan.follow_symlinks)
                .into_iter()
                .filter_entry(|e| !self.is_excluded(e.path()))
            {
                if let Ok(entry) = entry {
                    if entry.file_type().is_file() && self.should_scan_file(entry.path()) {
                        files.push(entry.path().to_path_buf());
                    }
                }
            }
        }

        Ok(files)
    }

    /// Check if a path should be excluded
    fn is_excluded(&self, path: &Path) -> bool {
        let path_str = path.to_string_lossy();

        for pattern in &self.config.scan.exclude {
            if let Ok(glob) = glob::Pattern::new(pattern) {
                if glob.matches(&path_str) {
                    return true;
                }
            }
        }

        false
    }

    /// Check if a file should be scanned based on extension and language filter
    fn should_scan_file(&self, path: &Path) -> bool {
        let ext = match path.extension().and_then(|e| e.to_str()) {
            Some(e) => e.to_lowercase(),
            None => return false,
        };

        // Map extensions to language names for filtering
        let lang_name = match ext.as_str() {
            "js" | "jsx" | "mjs" | "cjs" => "javascript",
            "ts" | "tsx" | "mts" | "cts" => "typescript",
            "py" | "pyw" | "pyi" => "python",
            "go" => "go",
            "java" => "java",
            "kt" | "kts" => "kotlin",
            "scala" | "sc" => "scala",
            "groovy" | "gvy" | "gy" | "gsh" => "groovy",
            "rs" => "rust",
            "c" | "h" => "c",
            "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "c++" | "h++" => "cpp",
            "cs" | "csx" => "csharp",
            "fs" | "fsx" | "fsi" => "fsharp",
            "php" | "phtml" | "php3" | "php4" | "php5" | "php7" | "phps" => "php",
            "rb" | "rbw" | "rake" | "gemspec" => "ruby",
            "swift" => "swift",
            "m" | "mm" => "objectivec",
            "sh" | "bash" | "zsh" | "ksh" | "fish" => "shell",
            "pl" | "pm" | "perl" => "perl",
            "lua" => "lua",
            "dart" => "dart",
            "ex" | "exs" => "elixir",
            "hs" | "lhs" => "haskell",
            "r" | "R" | "rmd" => "r",
            "clj" | "cljs" | "cljc" => "clojure",
            "sol" => "solidity",
            _ => return false, // Unsupported extension
        };

        // If no language filter is set, scan all supported languages
        if self.config.scan.languages.is_empty() {
            return true;
        }

        // Check if the file's language is in the allowed list
        self.config.scan.languages.iter().any(|allowed| {
            let allowed_lower = allowed.to_lowercase();
            // Support common aliases
            match allowed_lower.as_str() {
                "js" => lang_name == "javascript",
                "ts" => lang_name == "typescript",
                "py" => lang_name == "python",
                "c#" | "cs" => lang_name == "csharp",
                "c++" => lang_name == "cpp",
                "rb" => lang_name == "ruby",
                "sh" | "bash" => lang_name == "shell",
                "objc" | "objective-c" => lang_name == "objectivec",
                other => lang_name == other,
            }
        })
    }
}

/// Remote repository scanner
pub struct RemoteScanner {
    config: AppConfig,
}

impl RemoteScanner {
    pub fn new(config: AppConfig) -> Self {
        Self { config }
    }

    /// Clone and scan a remote Git repository
    pub async fn scan_remote(&self, url: &str, branch: Option<&str>) -> Result<(ScanResults, TempDir)> {
        println!(
            "{} {}",
            "Cloning repository:".blue().bold(),
            url.cyan()
        );

        // Create temp directory for clone
        let temp_dir = TempDir::new()?;
        let clone_path = temp_dir.path();

        // Clone the repository with progress
        self.clone_repository(url, clone_path, branch)?;

        println!(
            "{} {}",
            "Cloned to:".green(),
            clone_path.display().to_string().dimmed()
        );

        // Create scanner and scan the cloned repo
        let scanner = Scanner::new(self.config.clone());
        let results = scanner.scan(clone_path).await?;

        Ok((results, temp_dir))
    }

    /// Clone a Git repository to a local path
    fn clone_repository(&self, url: &str, path: &Path, branch: Option<&str>) -> Result<()> {
        // Set up progress callback
        let pb = ProgressBar::new(100);
        pb.set_style(
            ProgressStyle::default_bar()
                .template("{spinner:.green} [{bar:40.cyan/blue}] {msg}")
                .unwrap()
                .progress_chars("#>-"),
        );

        let mut callbacks = RemoteCallbacks::new();
        
        // Progress callback for network transfer
        callbacks.transfer_progress(|progress| {
            let received = progress.received_objects();
            let total = progress.total_objects();
            if total > 0 {
                let pct = (received as f64 / total as f64 * 100.0) as u64;
                pb.set_position(pct);
                pb.set_message(format!(
                    "Receiving objects: {}/{}",
                    received, total
                ));
            }
            true
        });

        // Set up fetch options
        let mut fetch_options = FetchOptions::new();
        fetch_options.remote_callbacks(callbacks);
        fetch_options.depth(1); // Shallow clone for speed

        // Build and execute clone
        let mut builder = RepoBuilder::new();
        builder.fetch_options(fetch_options);
        
        if let Some(b) = branch {
            builder.branch(b);
        }

        builder.clone(url, path)?;

        pb.finish_with_message("Clone complete");

        Ok(())
    }

    /// Parse a GitHub URL and extract owner/repo
    pub fn parse_github_url(url: &str) -> Option<(String, String)> {
        // Handle various GitHub URL formats:
        // - https://github.com/owner/repo
        // - https://github.com/owner/repo.git
        // - git@github.com:owner/repo.git
        // - owner/repo (shorthand)
        
        let url = url.trim();
        
        // Shorthand format: owner/repo
        if !url.contains("://") && !url.contains("@") && url.contains('/') {
            let parts: Vec<&str> = url.split('/').collect();
            if parts.len() == 2 {
                return Some((
                    parts[0].to_string(),
                    parts[1].trim_end_matches(".git").to_string(),
                ));
            }
        }

        // HTTPS format
        if url.starts_with("https://github.com/") {
            let path = url.trim_start_matches("https://github.com/");
            let parts: Vec<&str> = path.split('/').collect();
            if parts.len() >= 2 {
                return Some((
                    parts[0].to_string(),
                    parts[1].trim_end_matches(".git").to_string(),
                ));
            }
        }

        // SSH format: git@github.com:owner/repo.git
        if url.starts_with("git@github.com:") {
            let path = url.trim_start_matches("git@github.com:");
            let parts: Vec<&str> = path.split('/').collect();
            if parts.len() >= 2 {
                return Some((
                    parts[0].to_string(),
                    parts[1].trim_end_matches(".git").to_string(),
                ));
            }
        }

        None
    }

    /// Convert shorthand to full GitHub URL
    pub fn normalize_github_url(input: &str) -> String {
        let input = input.trim();
        
        // Already a full URL
        if input.starts_with("https://") || input.starts_with("git@") {
            return input.to_string();
        }

        // Shorthand format: owner/repo -> https://github.com/owner/repo.git
        if input.contains('/') && !input.contains(' ') {
            return format!("https://github.com/{}.git", input);
        }

        input.to_string()
    }
}
