# coding: utf-8

from collections.abc import Mapping
from copy import deepcopy
import os
import yaml
import re
from datetime import datetime
from snakemake.sourcecache import GithubFile, LocalGitFile, WorkflowError


# Dictionary mapping aligners to their path prefixes
ALIGNER_PATHS = {"minimap2": "alignment/minimap2_align",
                 "pbmm2": "alignment/pbmm2_align",
                 "vacmap": "alignment/vacmap_align",
                 "star": "alignment/star",
                 "parabricks_fq2bam": "parabricks/pbrun_fq2bam",
                 "parabricks_fq2bam_recal": "parabricks/pbrun_fq2bam_recal",
                 "parabricks_rna_fq2bam": "parabricks/pbrun_rna_fq2bam"}


def get_module_snakefile(config, repo, path, tag):
    ''' return a complete snakefile path, either pointing to github or a local repo'''
    if 'hydra_local_path' in config:
        repo_path = os.path.join(config['hydra_local_path'], repo)
        if not os.path.exists(repo_path):
            ' Remove possible organization part from repo path '
            repo_path_v2 = os.path.join(config['hydra_local_path'], os.sep.join(repo.split(os.sep)[1:]))
            if not os.path.exists(repo_path_v2):
                raise FileNotFoundError(f"Repo folder {repo_path} or {repo_path_v2} doesn't exist!")
            else:
                repo_path = repo_path_v2
        if not os.path.isdir(repo_path):
            raise FileNotFoundError(f"{repo_path} isn't a folder!")
        return LocalGitFile(repo_path, path=path, tag=tag)
    else:
        return GithubFile(repo, path=path, tag=tag)


def merge(dict1, dict2):
    ''' Return a new dictionary by merging two dictionaries recursively. '''

    result = deepcopy(dict1)
    for key, value in dict2.items():
        if isinstance(value, Mapping):
            result[key] = merge(result.get(key, {}), value)
        else:
            result[key] = deepcopy(dict2[key])
    return result


def extract_chr(file, filter_out=["chrM"]):
    chr = None
    if os.path.exists(file):
        with open(file) as lines:
            chr = [line.split("\t")[0] for line in lines]
        return [c for c in chr if c not in filter_out]
    return [""]


def replace_dict_variables(config):
    '''
        Make it possible to defina a variable in a  vaue string using ´{{}}´,
        ex {{PROJECT_VARIABLE}}. PROJECT_VARIABLE must exist as a variable
        in the yaml file and will be used to replace all occurences of
        {{PROJECT_VARAIBLE}} in the yaml file.
    '''
    def update_dict(temp_config):
        for k in temp_config:
            if type(temp_config[k]) is dict:
                temp_config[k] = update_dict(temp_config[k])
            elif type(temp_config[k]) is str:
                match = re.findall(r'\{\{([A-Za-z0-9_]+)\}\}', temp_config[k])
                for m in match:
                    temp_config[k] = temp_config[k].replace("{{" + m + "}}", config[m])
            elif type(temp_config[k]) is list:
                for i in range(len(temp_config[k])):
                    if type(temp_config[k][i]) is str:
                        match = re.findall(r'\{\{([A-Za-z0-9_]+)\}\}', temp_config[k][i])
                        for m in match:
                            temp_config[k][i] = temp_config[k][i].replace("{{" + m + "}}", config[m])
        return temp_config
    return update_dict(config)


def export_config_as_file(config, output_file="config", directory="versions", date_string=None):
    if date_string is None:
        date_string = datetime.now().strftime('%Y%m%d--%H-%M-%S')
    if directory is not None and len(directory) > 0:
        date_string = f"_{date_string}"
    if output_file is not None:
        output_file = f"{output_file}{date_string}.yaml"
    if directory is not None:
        output_file = os.path.join(directory, output_file)
    if len(directory) > 0 and not os.path.isdir(directory):
        os.makedirs(directory)
    with open(output_file, 'w') as writer:
        writer.write(yaml.dump(config))


def get_input_aligned_bam(wildcards, config, default_path="alignment/samtools_merge_bam"):
    """
    Compile the paths to input aligned BAM and BAI files for the workflow.

    This function determines the appropriate BAM file path based on the configuration
    and workflow parameters. Uses the ALIGNER_PATHS dictionary to map aligners to their
    path prefixes.

    Args:
        wildcards (snakemake.io.Wildcards): Wildcards object containing sample and type information.
        config (dict): Configuration dictionary with possible keys:
            - aligner (str or None): The aligner used for generating the BAM file.
        default_path (str): Default path for BAM files if no specific configuration is provided.

    Returns:
        tuple: A tuple containing the alignment BAM file path and its BAI index file path.
    """
    if config.get("aligner") is not None:
        # Use aligner to compile the paths using ALIGNER_PATHS dictionary
        aligner = config.get("aligner")
        path_prefix = ALIGNER_PATHS.get(aligner, f"alignment/{aligner}_align")
        alignment_path = f"{path_prefix}/{wildcards.sample}_{wildcards.type}.bam"
        index_path = f"{alignment_path}.bai"
    else:
        # no aligner, use the default path
        alignment_path = f"{default_path}/{wildcards.sample}_{wildcards.type}.bam"
        index_path = f"{alignment_path}.bai"

    return alignment_path, index_path


def get_input_haplotagged_bam(wildcards, config, default_path="alignment/samtools_merge_bam", suffix=None):
    """
    Compile paths to haplotagged BAM/BAI files (may be required for downstream analyses with e.g. cnvkit_batch)
    This function determines the appropriate BAM file path based on the configuration. suffix can be provided
    either as a function argument or in the config file under the key 'haplotag_suffix'.

    For backward compatibility, if only wildcards and config are provided, the function will default to using the
    default path and return 'alignment/samtools_merge_bam/{sample}_{type}.bam'.

    Args:
        wildcards (snakemake.io.Wildcards): Wildcards object containing sample and type information.
        config (dict): config with or without 'haplotag_path' key.
        default_path (str): Default path for BAM files if no specific configuration is provided.
        suffix (str): Suffix to append to the BAM file name (default is None).
    Returns:
        tuple: A tuple containing the alignment BAM file path and its BAI index file path.

    """
    try:
        sample_name = getattr(wildcards, "sample")
        sample_type = getattr(wildcards, "type")
    except AttributeError as e:
        raise WorkflowError(f"Missing required wildcards: {e}")

    haplotag_path = config.get("haplotag_path", None)
    if haplotag_path is not None:
        path_to_input_bam = haplotag_path
    elif config.get("aligner") is not None:
        aligner = config.get("aligner")
        path_to_input_bam = ALIGNER_PATHS.get(aligner, f"alignment/{aligner}_align")
    else:
        path_to_input_bam = default_path

    # check for suffix in the config
    if suffix is None:
        suffix = config.get("haplotag_suffix", None)

    if suffix:  # This will be False for None and ''
        file_name = f"{sample_name}_{sample_type}.{suffix}.bam"
    else:
        file_name = f"{sample_name}_{sample_type}.bam"

    bam_path = os.path.join(path_to_input_bam, file_name)
    bai_path = f"{bam_path}.bai"

    return bam_path, bai_path
