use std::{
    fs,
    io::{self, Read},
};

use anyhow::Result;
use clap::Parser;
use log::trace;
use paraseq::{fasta, Record};

use crate::{
    cli::FileFormat,
    commands::grep::{Pattern, PatternCollection, SimpleRange},
};

use super::{InputBinseq, OutputFile};

/// Grep a BINSEQ file and output to FASTQ or FASTA.
#[derive(Parser, Debug)]
pub struct GrepCommand {
    #[clap(flatten)]
    pub input: InputBinseq,

    #[clap(flatten)]
    pub output: OutputFile,

    #[clap(flatten)]
    pub grep: GrepArgs,
}
impl GrepCommand {
    pub fn should_color(&self) -> bool {
        match self.output.format() {
            Ok(FileFormat::Bam) => false,
            _ => {
                self.output.output.is_none()
                    && self.output.prefix.is_none()
                    && self.grep.color.should_color()
            }
        }
    }
}

#[derive(Parser, Debug)]
#[clap(next_help_heading = "SEARCH OPTIONS")]
pub struct GrepArgs {
    /// Regex expression to search for in primary sequence
    #[clap(short = 'r', long)]
    pub reg1: Vec<String>,

    /// Regex expression to search for in extended sequence
    #[clap(short = 'R', long)]
    pub reg2: Vec<String>,

    /// Regex expression to search for in either sequence
    pub reg: Vec<String>,

    /// Invert pattern criteria (like grep -v)
    #[clap(short = 'v', long)]
    pub invert: bool,

    /// Only count matches
    #[clap(short = 'C', long, conflicts_with = "pattern_count")]
    pub count: bool,

    /// Show match count as a fraction of total records
    ///
    /// Implies --count (-C). Displays the number of matches,
    /// total records, and the fraction of records matching.
    #[clap(short = 'F', long, conflicts_with = "pattern_count")]
    pub frac: bool,

    /// Only match patterns that are within this range.
    ///
    /// Will not match if the pattern is outside the range or if
    /// the sequence cannot be sliced within the range (i.e. out of bounds).
    ///
    /// Examples: --range=0..100, --range=..30, --range=60..
    #[clap(long)]
    pub range: Option<SimpleRange>,

    /// Count number of matches per pattern
    ///
    /// This will output a TSV with the number of matches per pattern.
    /// Note that a sequence may contribute to multiple patterns counts.
    /// A pattern will also only be counted once per sequence.
    #[clap(short = 'P', long, conflicts_with = "count")]
    pub pattern_count: bool,

    /// Denotes patterns are fixed strings (non-regex)
    ///
    /// Allows usage of Aho-Corasick algorithm for efficient matching.
    /// This is auto-detected when all patterns are literal strings.
    #[clap(short = 'x', long)]
    pub fixed: bool,

    /// Build Aho-Corasick automaton without DFA
    ///
    /// DFA uses more memory, but is significantly faster.
    #[clap(long)]
    pub no_dfa: bool,

    /// use OR logic for multiple patterns (default=AND)
    #[clap(long, conflicts_with = "pattern_count")]
    or_logic: bool,

    /// Colorize output (auto, always, never)
    #[clap(
        long,
        value_name = "WHEN",
        default_value = "auto",
        conflicts_with = "format"
    )]
    color: ColorWhen,

    #[cfg(feature = "fuzzy")]
    #[clap(flatten)]
    pub fuzzy_args: FuzzyArgs,

    #[clap(flatten)]
    pub file_args: PatternFileArgs,
}

impl GrepArgs {
    pub fn validate(&self) -> Result<()> {
        if self.reg1.is_empty()
            && self.reg2.is_empty()
            && self.reg.is_empty()
            && self.file_args.empty()
        {
            anyhow::bail!("At least one pattern must be specified");
        }
        Ok(())
    }
    pub fn and_logic(&self) -> bool {
        if self.file_args.empty() {
            !self.or_logic
        } else {
            // using any FILE args forces OR logic
            false
        }
    }
}

impl GrepArgs {
    fn chain_patterns(
        &self,
        cli_patterns: &[String],
        filetype: PatternFileType,
    ) -> Result<PatternCollection> {
        let cli_iter = cli_patterns.iter().map(|s| Pattern {
            name: None,
            sequence: s.as_bytes().to_vec(),
        });
        if self.file_args.empty_file(filetype) {
            Ok(PatternCollection(cli_iter.collect()))
        } else {
            let file_patterns = self.file_args.patterns(filetype)?;
            Ok(PatternCollection(cli_iter.chain(file_patterns).collect()))
        }
    }
    pub fn patterns_m1(&self) -> Result<PatternCollection> {
        self.chain_patterns(&self.reg1, PatternFileType::SFile)
    }
    pub fn patterns_m2(&self) -> Result<PatternCollection> {
        self.chain_patterns(&self.reg2, PatternFileType::XFile)
    }
    pub fn patterns(&self) -> Result<PatternCollection> {
        self.chain_patterns(&self.reg, PatternFileType::File)
    }
}

#[cfg(feature = "fuzzy")]
#[derive(Parser, Debug)]
#[clap(next_help_heading = "FUZZY MATCHING OPTIONS")]
pub struct FuzzyArgs {
    /// Fuzzy finding using `sassy`
    ///
    /// Note that regex expressions are not supported with this flag.
    #[clap(short = 'z', long)]
    pub fuzzy: bool,

    /// Maximum edit distance to allow when fuzzy matching
    ///
    /// Only used with fuzzy matching
    #[clap(short = 'k', long, default_value = "1")]
    pub distance: usize,

    /// Only return inexact matches on fuzzy matching
    ///
    /// This will capture matches that are not exact, but are within the specified edit distance.
    #[clap(short = 'i', long)]
    pub inexact: bool,
}

#[derive(Parser, Debug)]
#[clap(next_help_heading = "PATTERN FILE OPTIONS")]
pub struct PatternFileArgs {
    /// File of patterns to search for in either primary or extended sequence
    ///
    /// Accepts a plain text file (one pattern per line), a FASTA file
    /// (sequences are used as patterns), or TSV (alias / pattern).
    /// FASTA files and TSVs are auto-detected.
    /// Patterns may be regex or literal (fuzzy doesn't support regex).
    #[clap(long)]
    pub file: Option<String>,

    /// File of patterns to search for in primary sequence
    ///
    /// Accepts a plain text file (one pattern per line), a FASTA file
    /// (sequences are used as patterns), or TSV (alias / pattern).
    /// FASTA files and TSVs are auto-detected.
    /// Patterns may be regex or literal (fuzzy doesn't support regex).
    #[clap(long)]
    pub sfile: Option<String>,

    /// File of patterns to search for in extended sequence
    ///
    /// Accepts a plain text file (one pattern per line), a FASTA file
    /// (sequences are used as patterns), or TSV (alias / pattern).
    /// FASTA files and TSVs are auto-detected.
    /// Patterns may be regex or literal (fuzzy doesn't support regex).
    #[clap(long)]
    pub xfile: Option<String>,
}

impl PatternFileArgs {
    pub(crate) fn empty(&self) -> bool {
        self.file.is_none() && self.sfile.is_none() && self.xfile.is_none()
    }

    fn empty_file(&self, filetype: PatternFileType) -> bool {
        match filetype {
            PatternFileType::File => self.file.is_none(),
            PatternFileType::SFile => self.sfile.is_none(),
            PatternFileType::XFile => self.xfile.is_none(),
        }
    }

    fn file_path(&self, filetype: PatternFileType) -> Result<&str> {
        let file = match filetype {
            PatternFileType::File => &self.file,
            PatternFileType::SFile => &self.sfile,
            PatternFileType::XFile => &self.xfile,
        };
        file.as_deref()
            .ok_or_else(|| anyhow::anyhow!("Specified file type {filetype:?} not provided at CLI"))
    }

    /// Returns true if the file starts with '>' (FASTA format).
    fn is_fasta(path: &str) -> Result<bool> {
        let file = fs::File::open(path)?;
        // only take up to 10 bytes to determine fasta status
        for byte in io::BufReader::new(file).bytes().take(10) {
            let b = byte?;
            if b != b'\n' && b != b'\r' {
                return Ok(b == b'>');
            }
        }
        Ok(false)
    }

    /// Returns true if the file is a two-column TSV (tab-separated values)
    fn is_tsv(path: &str) -> Result<bool> {
        let mut reader = csv::ReaderBuilder::new()
            .delimiter(b'\t')
            .has_headers(false)
            .from_path(path)?;
        for res in reader.records().take(10) {
            let record = res?;
            if record.len() != 2 {
                return Ok(false);
            }
        }
        Ok(true)
    }

    /// Load patterns from a file, auto-detecting FASTA vs plain text.
    fn load_patterns(path: &str) -> Result<Vec<Pattern>> {
        if Self::is_fasta(path)? {
            trace!("Loading patterns from fasta: {path}");
            let mut reader = fasta::Reader::from_path(path)?;
            let mut rset = fasta::RecordSet::default();
            let mut patterns = Vec::new();

            while rset.fill(&mut reader)? {
                for record in rset.iter() {
                    let record = record?;
                    patterns.push(Pattern {
                        name: Some(record.id_str().to_string()),
                        sequence: record.seq().into_owned(),
                    });
                }
            }

            Ok(patterns)
        } else if Self::is_tsv(path)? {
            trace!("Loading alias+patterns from tsv: {path}");
            let mut reader = csv::ReaderBuilder::new()
                .delimiter(b'\t')
                .has_headers(false)
                .from_path(path)?;
            let mut patterns = Vec::new();
            for result in reader.records() {
                let record = result?;
                if record.len() != 2 {
                    anyhow::bail!("TSV file must have exactly two columns: name and pattern");
                }
                patterns.push(Pattern {
                    name: Some(record[0].to_string()),
                    sequence: record[1].as_bytes().to_vec(),
                });
            }
            Ok(patterns)
        } else {
            trace!("Loading patterns from txt: {path}");
            let contents = std::fs::read_to_string(path)?;
            Ok(contents
                .lines()
                .map(|line| Pattern {
                    name: None,
                    sequence: line.as_bytes().to_vec(),
                })
                .collect())
        }
    }

    pub fn patterns(&self, filetype: PatternFileType) -> Result<Vec<Pattern>> {
        let path = self.file_path(filetype)?;
        Self::load_patterns(path)
    }

    pub fn load_all_patterns(
        &self,
    ) -> Result<(PatternCollection, PatternCollection, PatternCollection)> {
        let pat1 = if let Some(ref path) = self.sfile {
            Self::load_patterns(path)?
        } else {
            Vec::default()
        };
        let pat2 = if let Some(ref path) = self.xfile {
            Self::load_patterns(path)?
        } else {
            Vec::default()
        };
        let pat = if let Some(ref path) = self.file {
            Self::load_patterns(path)?
        } else {
            Vec::default()
        };
        Ok((
            PatternCollection(pat1),
            PatternCollection(pat2),
            PatternCollection(pat),
        ))
    }
}

#[derive(Clone, Copy, Debug)]
pub enum PatternFileType {
    /// patterns for either primary or extended sequence
    File,
    /// primary sequence patterns
    SFile,
    /// extended sequence patterns
    XFile,
}

#[derive(Clone, Debug, clap::ValueEnum)]
pub enum ColorWhen {
    Auto,
    Always,
    Never,
}

impl ColorWhen {
    pub fn should_color(&self) -> bool {
        match self {
            ColorWhen::Always => true,
            ColorWhen::Never => false,
            ColorWhen::Auto => {
                use is_terminal::IsTerminal;
                std::io::stdout().is_terminal()
            }
        }
    }
}
