from functools import partial, reduce
from pathlib import Path
from typing import Callable, TypeAlias

from mmar_envops.exec_context import ExecContext
from mmar_envops.fs_utils import add_bak
from mmar_envops.stdout import build_diff
from mmar_envops.utils import noop
from mmar_envops.utils_collections import rfind

Op = tuple[str, ...]
Error = str
Either = tuple
Ignore: TypeAlias = str
Entry: TypeAlias = tuple[str, str]
RichEntry: TypeAlias = tuple[Ignore | None, Entry | None]


def parse_to_rich_entry(line: str) -> RichEntry:
    if not line or line[0] == "#" or "=" not in line:
        return line, None
    k, v = line.split("=", 1)
    return None, (k, v)


def dump_rich_entry(rentry: RichEntry) -> str:
    c, kv = rentry
    if c is not None:
        return c
    if kv is None:
        return ""
    k, v = kv
    return f"{k}={v}"


def get_key_from_rentry(rentry: RichEntry) -> str | None:
    c, kv = rentry
    if c is not None:
        return None
    if kv is None:
        return None
    k, v = kv
    return k


def is_rentry_has_key(rentry: RichEntry, key: str) -> bool:
    rentry_key = get_key_from_rentry(rentry)
    if rentry_key is None:
        return False
    return rentry_key.upper() == key.upper()


def update_rentry_value(rentry: RichEntry, key: str, value: str) -> RichEntry:
    c, kv = rentry
    if c is not None:
        return rentry
    if kv is None:
        return rentry
    k, v = kv
    assert key.lower() == k.lower()
    return None, (key, value)


class EnvFileAccessor:
    def __init__(self, ectx: ExecContext, env_file: Path, silent: bool = False) -> None:
        self.ectx = ectx
        self.env_file = env_file
        self.print = print if silent is False else noop

    def file(self):
        return self.env_file

    @property
    def env_file_old(self):
        return add_bak(self.env_file)

    def __str__(self):
        return f"EnvEditor({self.env_file})"

    def get_text(self):
        return self.env_file.read_text()

    def rg(self, pattern) -> str:
        cmd = f"rg --ignore-case '{pattern}' {self.env_file}"
        ex, lines = self.ectx.system_get_lines(cmd, cwd="/")
        if ex:
            error_msg = self.ectx.col.red(f"Failed to run cmd='{cmd}'")
            return str(error_msg)
        return "\n".join(lines or [])

    def _entries(self) -> list[tuple[str, str]]:
        lines = self.env_file.read_text().splitlines()
        entries = []
        for ln in lines:
            if "=" in ln and ln[0] != "#":
                parts = ln.split("=", 1)
                if len(parts) == 2:
                    entries.append((parts[0], parts[1]))
        return entries

    def _keys(self) -> list[str]:
        keys = [key for key, value in self._entries()]
        return keys

    def has_key(self, key) -> bool:
        keys = self._keys()
        return key.lower() in keys or key.upper() in keys

    def entries(self) -> str:
        return "\n".join([f"- {key} :: {value}" for key, value in self._entries()])

    def keys(self) -> str:
        return "\n".join(self._keys())

    def _get_value(self, key) -> str | None:
        value = next((v for k, v in self._entries() if key == k), None)
        return value

    def get_value(self, key, default: str | None = None) -> str | None:
        value = self._get_value(key.upper()) or self._get_value(key.lower())
        if value is None:
            return default
        return value

    def get_value_pretty(self, key) -> str:
        value = self._get_value(key.upper()) or self._get_value(key.lower())
        if value is None:
            not_found = self.ectx.col.red("Not found")
            return f"{not_found}: {key}"
        return value

    def get(self, key) -> str:
        value = self._get_value(key.upper()) or self._get_value(key.lower())
        if value is None:
            not_found = self.ectx.col.red("Not found")
            return f"{not_found}: {key}"
        return f"{key.upper()}={value}"

    def _do_change(self, transform_fn: Callable[[list[RichEntry]], list[RichEntry]]) -> bool:
        """Apply transformation to env file and return True if changed."""
        env_file_text = self.env_file.read_text().strip()
        env_file_lines = env_file_text.splitlines()
        env_rentries = list(map(parse_to_rich_entry, env_file_lines))
        env_rentries_fix = transform_fn(env_rentries)

        env_file_lines_fix = list(map(dump_rich_entry, env_rentries_fix))
        env_file_text_fix = "\n".join(env_file_lines_fix)

        if env_file_text_fix == env_file_text:
            return False

        if not self.ectx.dry:
            # todo fix duplicated idiom: {domain_root_context.py, domain_env_file_context.py}
            self.env_file_old.write_text(env_file_text)
            self.env_file.write_text(env_file_text_fix)
            self.print(f"Saved to {self.env_file}")
            self.print(f"Saved old version to {self.env_file_old}")

        # todo fix duplication related to build_diff: {compose_gen.py, domain_env_file_context.py}
        diff = build_diff(text_old=env_file_text, text=env_file_text_fix, col=self.ectx.col)
        self.print(diff)
        return not self.ectx.dry

    def _apply_set(self, env_rentries: list[RichEntry], key: str, value: str) -> list[RichEntry]:
        """Apply set operation to entries (always returns list)."""
        ii = rfind(env_rentries, partial(is_rentry_has_key, key=key))
        if ii is None:
            self.ectx.debug(f"Not found setting {key}, adding...")
            return env_rentries + [(None, (key, value))]
        rentry_ii = env_rentries[ii]
        comment, entry = rentry_ii
        assert comment is None and entry is not None, rentry_ii
        r_key, r_value = entry
        assert r_key.lower() == key.lower(), rentry_ii
        env_rentries[ii] = update_rentry_value(env_rentries[ii], key, value)
        return env_rentries

    def _apply_unset(self, env_rentries: list[RichEntry], key: str) -> list[RichEntry]:
        """Apply unset operation to entries (always returns list)."""
        ii = rfind(env_rentries, partial(is_rentry_has_key, key=key))
        assert ii is not None
        return env_rentries[:ii] + env_rentries[ii + 1 :]

    def _set_transform(self, env_rentries: list[RichEntry], key: str, value: str) -> list[RichEntry]:
        ii = rfind(env_rentries, partial(is_rentry_has_key, key=key))
        if ii is None:
            return self._apply_set(env_rentries, key, value)
        rentry_ii = env_rentries[ii]
        comment, entry = rentry_ii
        assert comment is None and entry is not None, rentry_ii
        r_key, r_value = entry
        assert r_key.lower() == key.lower(), rentry_ii
        if value == r_value:
            self.print(f"Nothing changes: already {key}={value}")
            return env_rentries
        return self._apply_set(env_rentries, key, value)

    def _unset_transform(self, env_rentries: list[RichEntry], key: str) -> list[RichEntry]:
        return self._apply_unset(env_rentries, key)

    def _interpret_op(self, op: Op) -> Callable[[list[RichEntry]], list[RichEntry]]:
        """Convert operation tuple into a transform function."""
        op_type = op[0]
        if op_type == "set":
            _, key, value = op
            key = self.ectx.ecfg.fix_key_case(key)
            return partial(self._apply_set, key=key, value=value)
        elif op_type == "unset":
            _, key = op
            return partial(self._apply_unset, key=key)
        else:
            raise ValueError(f"Unknown operation type: {op_type}")

    def do_batch(self, operations: list[Op]) -> bool:
        """Apply multiple operations in a single transaction.

        Operations format:
        - ("set", key, value) - set key=value
        - ("unset", key) - unset key

        Returns True if any changes were made.
        """
        ops_desc = ", ".join(str(op) for op in operations)
        self.print(f"Batch operations: {ops_desc}")

        transforms = [self._interpret_op(op) for op in operations]

        def _batch_transform(env_rentries: list[RichEntry]) -> list[RichEntry]:
            return reduce(lambda acc, fn: fn(acc), transforms, env_rentries)

        return self._do_change(_batch_transform)

    def set_key(self, key, value) -> bool:
        key = self.ectx.ecfg.fix_key_case(key)
        self.print(f"Passed key={key}, value={value}")
        return self._do_change(partial(self._set_transform, key=key, value=value))

    def unset_key(self, key):
        if not self.has_key(key):
            self.print(f"Nothing changes: no key {key} found")
            return

        self._do_change(partial(self._unset_transform, key=key))

    def _parse_key_line(self, key_line: str) -> tuple[str, str | None]:
        """Parse key_line into (key, value). Returns (key, None) if no '=' found."""
        if "=" in key_line:
            key, value = key_line.split("=", 1)
            return key, value
        return key_line, None

    def _find_entry_index_by_key_line(self, env_rentries: list[RichEntry], key: str, value: str | None) -> int | None:
        """Find index of an entry matching key=value (or just key if value is None)."""
        for i, rentry in enumerate(env_rentries):
            comment, entry = rentry
            if comment is None and entry is not None:
                k, v = entry
                if k.upper() == key.upper():
                    if value is None or v == value:
                        return i
        return None

    def _find_commented_index_by_key_line(
        self, env_rentries: list[RichEntry], key: str, value: str | None
    ) -> int | None:
        """Find index of a commented entry matching # key=value (or just key if value is None)."""
        for i, rentry in enumerate(env_rentries):
            comment, entry = rentry
            if comment is not None and entry is None:
                # Check if this comment is a commented-out key=value
                comment_stripped = comment.lstrip("#").strip()
                if "=" in comment_stripped:
                    k, v = comment_stripped.split("=", 1)
                    if k.upper() == key.upper():
                        if value is None or v == value:
                            return i
        return None

    def _apply_comment(self, env_rentries: list[RichEntry], key_line: str) -> list[RichEntry]:
        """Apply comment operation to entries (always returns list)."""
        key, value = self._parse_key_line(key_line)
        ii = self._find_entry_index_by_key_line(env_rentries, key, value)
        if ii is None:
            self.ectx.debug(f"Not found commenting {key_line}, nothing to do...")
            return env_rentries
        rentry_ii = env_rentries[ii]
        comment, entry = rentry_ii
        assert comment is None and entry is not None, rentry_ii
        # Convert entry to comment: (None, (key, value)) -> ("# key=value", None)
        commented_line = f"# {dump_rich_entry(rentry_ii)}"
        env_rentries[ii] = (commented_line, None)
        return env_rentries

    def _comment_transform(self, env_rentries: list[RichEntry], key_line: str) -> list[RichEntry]:
        return self._apply_comment(env_rentries, key_line)

    def comment_key(self, key_line):
        key, _ = self._parse_key_line(key_line)
        if not self.has_key(key):
            self.print(f"Nothing changes: no key {key} found")
            return

        key_line = self.ectx.ecfg.fix_key_case(key_line)
        self.print(f"Commenting out {key_line}")
        self._do_change(partial(self._comment_transform, key_line=key_line))

    def _apply_uncomment(self, env_rentries: list[RichEntry], key_line: str) -> list[RichEntry]:
        """Apply uncomment operation to entries (always returns list)."""
        key, value = self._parse_key_line(key_line)
        ii = self._find_commented_index_by_key_line(env_rentries, key, value)
        if ii is None:
            self.ectx.debug(f"Not found uncommenting {key_line}, nothing to do...")
            return env_rentries
        rentry_ii = env_rentries[ii]
        comment, entry = rentry_ii
        assert comment is not None and entry is None, rentry_ii
        # Convert comment to entry: ("# key=value", None) -> (None, (key, value))
        comment_stripped = comment.lstrip("#").strip()
        k, v = comment_stripped.split("=", 1)
        env_rentries[ii] = (None, (k, v))
        return env_rentries

    def _uncomment_transform(self, env_rentries: list[RichEntry], key_line: str) -> list[RichEntry]:
        return self._apply_uncomment(env_rentries, key_line)

    def uncomment_key(self, key_line):
        key_line = self.ectx.ecfg.fix_key_case(key_line)
        self.print(f"Uncommenting {key_line}")
        self._do_change(partial(self._uncomment_transform, key_line=key_line))
