use std::path::Path;

use anyhow::{bail, Context, Result};
use clap::Parser;
use tracing::{debug, info, warn};

use mitm2openapi::builder::{self, OpenApiBuilder};
use mitm2openapi::cli::{Cli, Command, InputFormat};
use mitm2openapi::har_reader;
use mitm2openapi::mitmproxy_reader;
use mitm2openapi::output;
use mitm2openapi::report::ProcessingReport;
use mitm2openapi::types::{CapturedRequest, Config};

fn main() {
    tracing_subscriber::fmt::init();

    let cli = Cli::parse();

    let result = run(cli);
    match result {
        Ok(exit_code) => std::process::exit(exit_code),
        Err(e) => {
            eprintln!("Error: {e:?}");
            std::process::exit(1);
        }
    }
}

fn run(cli: Cli) -> Result<i32> {
    match cli.command {
        Command::Discover(args) => {
            info!(input = %args.input.display(), output = %args.output.display(), "Starting discovery");

            let strict = args.strict;
            let mut report = ProcessingReport::new();
            report.input.path = args.input.display().to_string();
            report.input.format = format!("{:?}", args.format);
            if let Ok(meta) = std::fs::metadata(&args.input) {
                report.input.size_bytes = meta.len();
            }

            let req_iter = stream_input(
                &args.input,
                &args.format,
                args.max_input_size,
                args.allow_symlinks,
                args.max_payload_size as usize,
            )?;

            let counting_iter = CountingIterator::new(req_iter);
            let error_count = counting_iter.error_count.clone();

            let filtered_iter: Box<
                dyn Iterator<Item = mitm2openapi::error::Result<Box<dyn CapturedRequest>>>,
            > = if args.skip_options {
                Box::new(counting_iter.filter(|r| {
                    r.as_ref()
                        .map(|req| req.get_method().to_uppercase() != "OPTIONS")
                        .unwrap_or(true)
                }))
            } else {
                Box::new(counting_iter)
            };

            let custom_re = args
                .param_regex
                .as_deref()
                .map(|pat| {
                    regex::Regex::new(pat)
                        .with_context(|| format!("invalid --param-regex pattern: {pat}"))
                })
                .transpose()?;

            let templates = builder::discover_paths_streaming(
                filtered_iter,
                &args.prefix,
                custom_re.as_ref(),
                &args.exclude_patterns,
                &args.include_patterns,
            );

            let errors_seen = error_count.get();
            if errors_seen > 0 {
                report
                    .events
                    .parse_error
                    .insert("parse errors during discovery".to_string(), errors_seen);
            }

            let merged = if args.output.exists() {
                let existing = load_templates(&args.output)
                    .context("failed to load existing templates file")?;
                debug!(
                    existing = existing.len(),
                    new = templates.len(),
                    "Merging templates"
                );
                merge_templates(&existing, &templates)
            } else {
                templates
            };

            report.result.paths_in_spec = merged.len() as u64;

            let yaml = output::templates_to_yaml(&merged)?;
            output::write_yaml(&yaml, &args.output)?;

            if let Some(report_path) = &args.report {
                report.write_to_path(report_path).with_context(|| {
                    format!("failed to write report to {}", report_path.display())
                })?;
                info!(path = %report_path.display(), "Processing report written");
            }

            info!(
                count = merged.len(),
                output = %args.output.display(),
                "Discovery complete"
            );
            eprintln!(
                "Discovered {} path template(s), written to {}",
                merged.len(),
                args.output.display()
            );

            check_strict(strict, &report)
        }
        Command::Generate(args) => {
            info!(input = %args.input.display(), output = %args.output.display(), "Starting generation");

            let strict = args.strict;
            let mut report = ProcessingReport::new();
            report.input.path = args.input.display().to_string();
            report.input.format = format!("{:?}", args.format);
            if let Ok(meta) = std::fs::metadata(&args.input) {
                report.input.size_bytes = meta.len();
            }

            let req_iter = stream_input(
                &args.input,
                &args.format,
                args.max_input_size,
                args.allow_symlinks,
                args.max_payload_size as usize,
            )?;

            let all_templates = load_templates(&args.templates).with_context(|| {
                format!("failed to load templates from {}", args.templates.display())
            })?;
            let active_templates: Vec<String> = all_templates
                .into_iter()
                .filter(|t| !t.starts_with("ignore:"))
                .collect();

            if active_templates.is_empty() {
                bail!(
                    "No active templates found in {}. Remove the 'ignore:' prefix from paths you want to include.",
                    args.templates.display()
                );
            }

            info!(count = active_templates.len(), "Using active templates");

            let config = Config {
                prefix: args.prefix.clone(),
                openapi_title: args.openapi_title.clone(),
                openapi_version: args.openapi_version.clone(),
                exclude_headers: parse_comma_list(&args.exclude_headers),
                exclude_cookies: parse_comma_list(&args.exclude_cookies),
                include_headers: args.include_headers,
                ignore_images: args.ignore_images,
                suppress_params: args.suppress_params,
                tags_overrides: args.tags_overrides.clone(),
                skip_options: args.skip_options,
                max_examples: args.max_examples,
                redact_patterns: args.redact_patterns.clone(),
                redact_fields: args.redact_fields.clone(),
            };

            let mut builder = OpenApiBuilder::new(&args.prefix, &config, active_templates);
            let mut count = 0usize;
            for req_result in req_iter {
                report.result.flows_read += 1;
                match req_result {
                    Ok(req) => {
                        builder.add_request(req.as_ref());
                        report.result.flows_emitted += 1;
                        count += 1;
                    }
                    Err(e) => {
                        let error_key = format!("{}", e);
                        *report.events.parse_error.entry(error_key).or_insert(0) += 1;
                        warn!(error = %e, "Skipping request due to error");
                    }
                }
            }
            info!(count, path = %args.input.display(), "Processed requests");
            let spec = builder.build();

            let path_count = spec.paths.paths.len();
            report.result.paths_in_spec = path_count as u64;

            let yaml = output::spec_to_yaml(&spec)?;
            output::write_yaml(&yaml, &args.output)?;

            if let Some(report_path) = &args.report {
                report.write_to_path(report_path).with_context(|| {
                    format!("failed to write report to {}", report_path.display())
                })?;
                info!(path = %report_path.display(), "Processing report written");
            }

            info!(
                paths = path_count,
                output = %args.output.display(),
                "Generation complete"
            );
            eprintln!(
                "Generated OpenAPI spec with {} path(s), written to {}",
                path_count,
                args.output.display()
            );

            check_strict(strict, &report)
        }
    }
}

fn check_strict(strict: bool, report: &ProcessingReport) -> Result<i32> {
    if strict {
        let event_total = report.events.total();
        if event_total > 0 {
            eprintln!(
                "strict mode: {} event(s) recorded (caps fired, rejections, or parse errors); exiting with code 2",
                event_total
            );
            return Ok(2);
        }
    }
    Ok(0)
}

use std::cell::Cell;

struct CountingIterator {
    inner: RequestIter,
    error_count: std::rc::Rc<Cell<u64>>,
}

impl CountingIterator {
    fn new(inner: RequestIter) -> Self {
        Self {
            inner,
            error_count: std::rc::Rc::new(Cell::new(0)),
        }
    }
}

impl Iterator for CountingIterator {
    type Item = mitm2openapi::error::Result<Box<dyn CapturedRequest>>;

    fn next(&mut self) -> Option<Self::Item> {
        let item = self.inner.next()?;
        if item.is_err() {
            self.error_count.set(self.error_count.get() + 1);
        }
        Some(item)
    }
}

/// Heuristic scoring for format auto-detection.
/// Returns (mitmproxy_score, har_score) — higher score means more confidence.
fn detect_format_score(path: &Path) -> (u8, u8) {
    let mut mitmproxy_score: u8 = 0;
    let mut har_score: u8 = 0;

    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
        match ext.to_lowercase().as_str() {
            "flow" => mitmproxy_score += 3,
            "har" => har_score += 3,
            _ => {}
        }
    }

    if mitmproxy_reader::mitmproxy_heuristic(path) {
        mitmproxy_score += 2;
    }
    if har_reader::har_heuristic(path) {
        har_score += 2;
    }

    (mitmproxy_score, har_score)
}

type RequestIter = Box<dyn Iterator<Item = mitm2openapi::error::Result<Box<dyn CapturedRequest>>>>;

fn stream_input(
    path: &Path,
    format: &InputFormat,
    max_input_size: u64,
    allow_symlinks: bool,
    max_payload_size: usize,
) -> Result<RequestIter> {
    // Check symlink-ness before is_dir(), since is_dir() follows symlinks.
    if !allow_symlinks {
        if let Ok(meta) = path.symlink_metadata() {
            if meta.file_type().is_symlink() {
                return Err(mitm2openapi::error::Error::SymlinkRejected {
                    path: path.to_path_buf(),
                }
                .into());
            }
        }
    }
    if !path.is_dir() {
        mitm2openapi::validate_input_path(path, max_input_size, allow_symlinks)
            .context("input file validation failed")?;
    }
    let reject_symlinks = !allow_symlinks;
    match format {
        InputFormat::Mitmproxy => {
            debug!(path = %path.display(), "Streaming as mitmproxy format");
            if path.is_dir() {
                if reject_symlinks {
                    mitmproxy_reader::stream_mitmproxy_dir_no_symlinks(path, max_payload_size)
                } else {
                    mitmproxy_reader::stream_mitmproxy_dir(path, max_payload_size)
                }
                .context("failed to stream mitmproxy directory")
            } else {
                let iter = mitmproxy_reader::stream_mitmproxy_file(path, max_payload_size)
                    .context("failed to stream mitmproxy file")?;
                Ok(Box::new(iter))
            }
        }
        InputFormat::Har => {
            debug!(path = %path.display(), "Streaming as HAR format");
            let iter = har_reader::stream_har_file(path).context("failed to stream HAR file")?;
            Ok(Box::new(iter))
        }
        InputFormat::Auto => {
            if path.is_dir() {
                debug!(path = %path.display(), "Auto-detecting format for directory");
                let mitmproxy_result = if reject_symlinks {
                    mitmproxy_reader::stream_mitmproxy_dir_no_symlinks(path, max_payload_size)
                } else {
                    mitmproxy_reader::stream_mitmproxy_dir(path, max_payload_size)
                };
                let har_result = if reject_symlinks {
                    har_reader::stream_har_dir_no_symlinks(path)
                } else {
                    har_reader::stream_har_file(path)
                };

                match (mitmproxy_result, har_result) {
                    (Ok(m_iter), Ok(h_iter)) => {
                        let combined: RequestIter = Box::new(m_iter.chain(h_iter));
                        Ok(combined)
                    }
                    (Ok(m_iter), Err(_)) => Ok(m_iter),
                    (Err(_), Ok(h_iter)) => Ok(h_iter),
                    (Err(e1), Err(_e2)) => {
                        Err(e1).context("failed to read directory as mitmproxy or HAR")
                    }
                }
            } else {
                let (ms, hs) = detect_format_score(path);
                debug!(
                    path = %path.display(),
                    mitmproxy_score = ms,
                    har_score = hs,
                    "Format auto-detection scores"
                );

                if ms > hs {
                    info!(path = %path.display(), "Auto-detected as mitmproxy format");
                    let iter = mitmproxy_reader::stream_mitmproxy_file(path, max_payload_size)
                        .context("detected as mitmproxy format but failed to parse")?;
                    Ok(Box::new(iter))
                } else if hs > ms {
                    info!(path = %path.display(), "Auto-detected as HAR format");
                    let iter = har_reader::stream_har_file(path)
                        .context("detected as HAR format but failed to stream")?;
                    Ok(Box::new(iter))
                } else if ms > 0 {
                    warn!(path = %path.display(), "Ambiguous format detection, trying mitmproxy first");
                    match mitmproxy_reader::stream_mitmproxy_file(path, max_payload_size) {
                        Ok(iter) => Ok(Box::new(iter)),
                        Err(_) => {
                            let iter = har_reader::stream_har_file(path)
                                .context("failed to parse as either mitmproxy or HAR")?;
                            Ok(Box::new(iter))
                        }
                    }
                } else {
                    bail!(
                        "Cannot auto-detect format for '{}'. Use --format to specify.",
                        path.display()
                    );
                }
            }
        }
    }
}

fn load_templates(path: &Path) -> Result<Vec<String>> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read templates file: {}", path.display()))?;
    let yaml: serde_yaml_ng::Value =
        serde_yaml_ng::from_str(&content).context("failed to parse templates YAML")?;

    let templates = yaml
        .get("x-path-templates")
        .and_then(|v| v.as_sequence())
        .map(|seq| {
            seq.iter()
                .filter_map(|v| v.as_str().map(|s| s.to_string()))
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();

    Ok(templates)
}

fn merge_templates(existing: &[String], new: &[String]) -> Vec<String> {
    use std::collections::BTreeSet;

    let existing_paths: BTreeSet<String> = existing
        .iter()
        .map(|t| t.strip_prefix("ignore:").unwrap_or(t).to_string())
        .collect();

    let mut merged: Vec<String> = existing.to_vec();

    for t in new {
        let bare = t.strip_prefix("ignore:").unwrap_or(t);
        if !existing_paths.contains(bare) {
            merged.push(t.clone());
        }
    }

    merged
}

fn parse_comma_list(opt: &Option<String>) -> Vec<String> {
    opt.as_deref()
        .map(|s| {
            s.split(',')
                .map(|s| s.trim().to_string())
                .filter(|s| !s.is_empty())
                .collect()
        })
        .unwrap_or_default()
}
