import re
from pathlib import Path
from typing import Dict, Any, List, Union

import yaml

from metacarp.app.command import Command
from metacarp.app.utils.loggingutils import logger


def parse_illumina_pairs(ilmn_dir: Path) -> dict:
    """
    Parse Illumina paired-end files using pattern matching.
    Supports standard Illumina naming and simple R1/R2 naming.
    """
    input_dict = {}

    # Pattern 1: Standard Illumina naming (e.g., Sample_S1_L001_R1_001.fastq.gz)
    illumina_pattern = re.compile(r'^(.+?)_S\d+_L\d+_R[12]_\d+\.fastq\.gz$')

    # Pattern 2: Simple R1/R2 naming (e.g., Sample_1.fastq.gz or Sample_R1.fastq.gz)
    simple_pattern = re.compile(r'^(.+?)_[R]?[12]\.fastq\.gz$')

    files = sorted(ilmn_dir.glob('*.fastq.gz'))
    processed = set()

    for fq_file in files:
        if fq_file in processed:
            continue

        # Try standard Illumina format
        match = illumina_pattern.match(fq_file.name)
        if match:
            sample_name = match.group(1)
            r1_pattern = f"{sample_name}_S*_L*_R1_*.fastq.gz"
            r2_pattern = f"{sample_name}_S*_L*_R2_*.fastq.gz"
        else:
            # Try simple format
            match = simple_pattern.match(fq_file.name)
            if not match:
                logger.warning(f"Skipping file with unrecognized format: {fq_file.name}")
                continue
            sample_name = match.group(1)
            r1_pattern = f"{sample_name}_[R]?1.fastq.gz"
            r2_pattern = f"{sample_name}_[R]?2.fastq.gz"

        r1_files = list(ilmn_dir.glob(r1_pattern))
        r2_files = list(ilmn_dir.glob(r2_pattern))

        if len(r1_files) == 1 and len(r2_files) == 1:
            input_dict[sample_name] = {
                'fq_fw': str(r1_files[0].absolute()),
                'fq_rv': str(r2_files[0].absolute()),
                'tech': 'ilmn'
            }
            processed.update([r1_files[0], r2_files[0]])
        else:
            logger.warning(f"Could not find paired files for sample: {sample_name}")

    return input_dict

def generate_config_file(config_data: Dict[str, Any], output_dir: Path, output_basename: str = 'config.yml') -> str:
    """
    Generates a configuration file for Snakemake in YAML file format.
    :param config_data: Configuration data
    :param output_dir: Output directory
    :param output_basename: Output basename
    :return: Path to config file
    """
    config_path = output_dir / output_basename
    if not output_dir.exists():
        output_dir.mkdir(parents=True)
    with config_path.open('w') as handle:
        yaml.dump(config_data, handle)
    logger.info(f"Configuration file created: {config_path}")
    return str(config_path)


def run_snakemake(snakefile: Path, config_path: Path, targets: List[Path], dir_: Path, threads: int = 8) -> Command:
    """
    Helper function to run snakemake workflows.
    :param snakefile: Workflow snakefile
    :param config_path: Path to configuration file
    :param targets: Target output files
    :param dir_: Working directory
    :param threads: Number of threads to use
    :return: Snakemake command
    """
    # Create working directory
    if not dir_.exists():
        logger.debug(f'Creating working directory: {dir_}')
        dir_.mkdir(parents=True)

    # Create and run command
    command = Command(' '.join([
        'snakemake', *[str(x) for x in targets], '--snakefile', str(snakefile), '--configfile', str(config_path),
        '--cores', str(threads)]))
    command.run(dir_)

    # Check if command executed successfully
    if command.exit_code != 0:
        rule_failed = __get_failed_rule(command.stderr)
        logger.error(f"Failed at rule: {rule_failed if rule_failed is not None else 'n/a'}")
        raise RuntimeError(f'Error executing Snakemake')
    return command


def __get_failed_rule(stderr: str) -> Union[str, None]:
    """
    Returns the name of the rule that failed during Snakemake execution.
    :return: Name of the failed rule
    """
    for line in reversed(stderr.splitlines()):
        m = re.match(r'Error in rule (\w+):', line.strip())
        if not m:
            continue
        return m.group(1)
