import os
from pathlib import Path
import re
import shutil
import sys
import zipfile
import time
import subprocess
import platform
from typing import Any, Generator

import polars as pl


# polars functions

def check_duplicates(df: pl.DataFrame, *id_cols: str) -> None:
    """Throw an error if duplicates are found in the dataframe."""
    duplicates = df.group_by(id_cols).count().filter(pl.col("count") > 1)
    if len(duplicates) > 0:
        raise ValueError(f"Des doublons ont été trouvés :\n{duplicates}")


def to_path(value: Path|str) -> Path:
    """Cast a string to a Path object if it's a string."""
    return value if isinstance(value, Path) else Path(value)


# files functions

def replace_in_file(filepath: Path, repl_mapping: dict[str, str], file_suffix: str = "~") -> Path:
    """Replace expressions in a file and save the result to a new file."""
    with open(str(filepath), "r", encoding="utf8") as f:
        data = f.read()
    for text_to_replace, replaced_by in repl_mapping.items():
        new_data = data.replace(text_to_replace, replaced_by)
    new_filepath = filepath.with_name(f"{filepath.stem}~{filepath.suffix}") if file_suffix else filepath
    with open(str(new_filepath), "w", encoding="utf8") as f:
        f.write(new_data)
    return new_filepath


def unzip(zip_filepath: Path|str) -> str:
    """Unzips a .zip file and returns the path to the extracted folder."""
    destination_folder = os.path.splitext(zip_filepath)[0]  # Remove the .zip extension
    with zipfile.ZipFile(zip_filepath, 'r') as zip_ref:  # Extract the zip file into the destination folder
        zip_ref.extractall(destination_folder)
        return destination_folder


def watch_file(filepath: str):
    """Watch a file for changes and return when it is modified."""
    last_modif = os.path.getmtime(filepath)
    while True:
        time.sleep(2)  # Attente avant de vérifier à nouveau
        new_modif = os.path.getmtime(filepath)
        if new_modif != last_modif:
            break


def open_file(file_path):
    """Opens the given file with its default editor."""
    if platform.system() == "Windows":
        os.startfile(file_path)
    elif platform.system() == "Darwin":  # macOS
        subprocess.run(["open", file_path])
    else:  # Linux
        subprocess.run(["xdg-open", file_path])


# string functions

def replace_in_expr(expr: str, repl_mapping: dict[str, str]) -> str:
    """Replace expressions in another expression."""
    if not new_part in expr:
        for old_part, new_part in repl_mapping.items():
            expr = expr.replace(old_part, new_part)
        return expr
    return expr


def anonymize(text: str) -> str:
    """Anonymize sensitive information in a text by replacing it with asterisks."""
    PHONE_REGEX = r"\+?\d{1,4}[\s.-]?\(?\d{1,4}\)?[\s.-]?\d{1,4}[\s.-]?\d{1,4}[\s.-]?\d{1,9}"
    EMAIL_REGEX = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
    if text is None:
        return None
    text = re.sub(EMAIL_REGEX, "**********", text)
    text = re.sub(PHONE_REGEX, "**********", text)
    return text


def percentage_difference(str1: str, str2: str) -> float:
    """Calculate the percentage difference between two strings using Levenshtein distance."""
    from rapidfuzz.distance import Levenshtein
    max_len = max(len(str1), len(str2))
    if max_len == 0:
        return 0  # Avoid division by zero
    return (Levenshtein.distance(str1, str2) / max_len) * 100


# Path functions

def replace_home_token_by_home_path(path: str) -> str:
    """Replace <HOME> by home path"""
    return path.replace("<HOME>", get_home_path())


def get_home_path() -> str:
    """Get the home path"""
    return str(Path.home())


def extract_subfolder_contents(parent_folder: str) -> None:
    """Moves contents from a uniquely named subfolder to `parent_folder` and deletes the subfolder if empty."""
    subfolders = [f for f in os.listdir(parent_folder) if os.path.isdir(os.path.join(parent_folder, f))]  # Get subfolders inside parent_folder
    if len(subfolders) == 1 and subfolders[0] == os.path.basename(parent_folder):  # Ensure there is **only one** subfolder and its name matches `parent_folder`
        subfolder_path = os.path.join(parent_folder, subfolders[0])
        for item in os.listdir(subfolder_path):  # Move all contents to parent folder
            shutil.move(os.path.join(subfolder_path, item), parent_folder)
        os.rmdir(subfolder_path)  # Remove the now-empty subfolder


def iter_all_files(root_folder: Path|str) -> Generator[Path, None, None]:
    """Recursively yields file paths inside subdirectories of `root_folder`."""
    for folder, _, files in os.walk(root_folder):
        for file in files:
            yield Path(os.path.join(folder, file))  # Yield file paths one by one


def is_only_extension(path) -> bool:
    """Check if the path has only one component and is a file extension."""
    return len(path) > 1 and path[0] == '.' and "/" not in path


def change_suffix(filepath: str, new_suffix: str):
    """Change the file extension while preserving the original name."""
    filepath = Path(filepath)
    filepath = filepath.with_suffix(new_suffix)
    return filepath


def complete_path_with_workdir(filepath: str|Path) -> Path:
    """Complete the file path with the pitchoune working directory."""
    workdir = os.getenv("PITCHOUNE_WORKDIR")
    return workdir / to_path(filepath) if workdir and not os.path.isabs(filepath) else to_path(filepath)


def replace_conf_key_by_conf_value(filepath: str) -> str:
    """Replace conf: by conf value"""
    if filepath.startswith("conf:"):
        return load_from_conf(filepath.removeprefix("conf:"))
    return filepath


def replace_by_module_name_if_only_extension(filepath: str):
    """Replace for example .xlsx by example.xlsx if the main module name is example"""
    return (get_main_module_name() + filepath) if is_only_extension(filepath) else filepath


def load_from_conf(key: str, filename: str = None, ignore_errors: bool = False, default_value: Any = None) -> Any:
    """Load from conf file"""
    if not filename:
        filename = os.getenv("PITCHOUNE_WORKDIR") + "\\.conf"
    try:
        with open(filename, "r") as file:
            for line in file:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                if "=" in line:
                    k, v = line.split("=", 1)
                    if k.strip() == key:
                        v = v.strip()
                        return v.strip() or None
        if not ignore_errors:
            raise KeyError(f"Key '{key}' not found into the file '{filename}'")
    except FileNotFoundError:
        if not ignore_errors:
            raise FileNotFoundError(f"File '{filename}' not found")
    return default_value


def enrich_path(path: str | Path) -> str:
    """Enrich path"""
    path = str(path)
    path = replace_conf_key_by_conf_value(path)
    path = replace_home_token_by_home_path(path)
    path = replace_by_module_name_if_only_extension(path)
    path = complete_path_with_workdir(path)
    return path


# other functions


def get_main_module_name() -> str:
    """Get the name of the main module (script) without the file extension."""
    return os.path.splitext(os.path.basename(sys.modules["__main__"].__file__))[0]

