import logging
import re
import sys
from io import StringIO
from pathlib import Path
from typing import Any, Optional, Union, MutableMapping, MutableSequence

import ruamel.yaml
from ruamel.yaml.comments import CommentedBase, Comment

from yamlex.api.exceptions import (
    NoValidVersionNumber,
    ExtensionDirNotFound,
    FailedToReadVersionFile,
    FailedToWriteVersionFile,
)


logger = logging.getLogger(__name__)
parser = ruamel.yaml.YAML()


def adjust_root_logger(verbose: bool = False, quiet: bool = False) -> None:
    if quiet:
        logging.root.setLevel(logging.ERROR)
    elif verbose:
        logging.root.setLevel(logging.DEBUG)


def indent(num: int = 0) -> str:
    return "· " * num


def get_default_extension_source_dir_path() -> Path:
    src_dir_path = Path("src")
    if src_dir_path.exists() and src_dir_path.is_dir():
        return src_dir_path / "source"
    else:
        return Path("source")


def get_default_extension_dir_path() -> Path:
    candidates = [
        Path("extension"),
        Path("src/extension"),
    ]

    # Find the first suitable candidate
    for candidate in candidates:
        # Check the candidate directory exists
        if candidate.exists() and candidate.is_dir():
            return candidate
        continue

    # Nothing suitable was found
    logger.error(
        (
            "No default extension directory found. "
            "'extension/' or 'src/extension/' folder must exist "
            "in the current working directory."
        )
    )
    raise ExtensionDirNotFound()


def is_manually_created(path: Path) -> bool:
    if path.exists():
        with open(path, "r") as f:
            content = f.read().lower()
            if "generated by yamlex" not in content:
                True
    return False


def sanitize_file_stem(stem: str) -> Path:
    valid_id = re.sub(r"[^a-z0-9\.]", "_", stem.lower())
    return valid_id


def write_file(
    file_path: Path,
    data: Union[dict, list],
    add_file_header: bool = True,
    line_length: Optional[int] = None,
    dry_run: bool = False,
    print_to_stdout: bool = False,
) -> None:
    # Convert dict to YAML. Dump to string first to add a comment
    stream = StringIO()
    parser.indent(mapping=2, sequence=4, offset=2)
    parser.width = line_length or sys.maxsize
    parser.dump(data, stream)
    text = stream.getvalue()

    # Write a comment to indicate that the file was automatically generated
    header = f"# Generated by yamlex\n\n"

    # Dry run simply means we are not creating the files
    if not dry_run:
        # Make sure the directory we write to exists
        dir = file_path.parent.resolve()
        dir.mkdir(parents=True, exist_ok=True)

        # Write to output file
        with open(file_path, "w") as f:
            if add_file_header:
                f.write(header)
            f.write(text)

    # If dry run mode is active then we might be asked to print to stdout
    elif print_to_stdout:
        if add_file_header:
            print(header, end=None)
        print(text)


def read_version_properties(path: Path, default: Optional[str] = None) -> str:
    try:
        with open(path, "r") as f:
            content = f.read()
            match_ = re.search(r"version=(.*)", content)
            if not match_ and not default:
                raise FailedToReadVersionFile(path)
            return match_.group(1)
    except Exception as e:
        logger.error(f"Failed to open {path}: {e}")
        raise FailedToReadVersionFile(e)


def write_version_properties(path: Path, version: str) -> None:
    try:
        with open(path, "r") as rf:
            lines = rf.readlines()
            content = []
            for l in lines:
                if l.startswith("version"):
                    content.append(f"version={version}\n")
                else:
                    content.append(l)
        with open(path, "w") as wf:
            wf.writelines(content) 
    except Exception as e:
        logger.error(f"Failed to write to {path}: {e}")
        raise FailedToWriteVersionFile(e)


def parse_version(version: str) -> Optional[str]:
    regex = r"^(0|[1-9][0-9]{0,3})(\.(0|[1-9][0-9]{0,3})){0,2}$"
    match_ = re.search(regex, version)
    if not match_:
        return None
    return match_.group()


def bump_version(version: str) -> str:
    version_parts = version.split(".")
    part_int: int | None = None
    part_idx: int | None = None
    for i, part in enumerate(reversed(version_parts)):
        try:
            part_int = int(part)
            part_idx = len(version_parts) - 1 - i
            break
        except:
            continue
    if part_int is None:
        logger.error("Could not find integer part in version")
        raise NoValidVersionNumber()

    part_int += 1
    result = ""
    for i, part in enumerate(version_parts):
        append = part
        if i == part_idx:
            append = part_int
        result += f"{append}."
    result = result.rstrip(".")
    return result


def remove_yaml_comments(
    key: str,
    obj: Any,
    recursive: bool = False,
    level: int = 0,
) -> Any:
    indentation = indent(level)

    if isinstance(obj, CommentedBase):
        if hasattr(obj, Comment.attrib):
            delattr(obj, Comment.attrib)
            logger.debug(f"{indentation}Removed comments from {str(key)}")

    if isinstance(obj, MutableMapping):
        result = {}
        for k, v in obj.items():
            result[k] = remove_yaml_comments(
                k,
                v,
                recursive=True,
                level=level + 1,
            ) if recursive else v
        return result

    if isinstance(obj, MutableSequence):
        result = []
        for i, v in enumerate(obj):
            result.append(
                remove_yaml_comments(
                    i,
                    v,
                    recursive=True,
                    level=level + 1,
                ) if recursive else v
            )
        return result

    return obj
