"""Minimal Newick parsing and utilities."""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path


@dataclass
class TreeNode:
    name: str | None = None
    length: float = 0.0
    children: list[TreeNode] = field(default_factory=list)

    def is_leaf(self) -> bool:
        return len(self.children) == 0


@dataclass
class PhyloTree:
    root: TreeNode

    def leaves(self) -> list[TreeNode]:
        out: list[TreeNode] = []

        def walk(node: TreeNode) -> None:
            if node.is_leaf():
                out.append(node)
                return
            for child in node.children:
                walk(child)

        walk(self.root)
        return out

    def leaf_names(self) -> list[str]:
        names = [leaf.name for leaf in self.leaves() if leaf.name]
        if len(names) != len(self.leaves()):
            raise ValueError("All leaf nodes must be named")
        return names

    def branch_lengths(self) -> list[float]:
        vals: list[float] = []

        def walk(node: TreeNode) -> None:
            for child in node.children:
                vals.append(child.length)
                walk(child)

        walk(self.root)
        return vals


class _NewickParser:
    def __init__(self, text: str):
        self.text = text.strip()
        if not self.text.endswith(";"):
            raise ValueError("Newick tree must end with ';'")
        self.i = 0

    def parse(self) -> TreeNode:
        node = self._parse_subtree()
        self._consume_whitespace()
        if self._peek() != ";":
            raise ValueError("Unexpected trailing Newick content")
        self.i += 1
        return node

    def _parse_subtree(self) -> TreeNode:
        self._consume_whitespace()
        if self._peek() == "(":
            self.i += 1
            children = [self._parse_subtree()]
            while True:
                self._consume_whitespace()
                ch = self._peek()
                if ch == ",":
                    self.i += 1
                    children.append(self._parse_subtree())
                    continue
                if ch == ")":
                    self.i += 1
                    break
                raise ValueError(f"Unexpected token in Newick at position {self.i}: {ch!r}")
            name = self._parse_name()
            length = self._parse_length()
            return TreeNode(name=name, length=length, children=children)

        name = self._parse_name(required=True)
        length = self._parse_length()
        return TreeNode(name=name, length=length)

    def _parse_name(self, required: bool = False) -> str | None:
        self._consume_whitespace()
        start = self.i
        while self.i < len(self.text) and self.text[self.i] not in [":", ",", ")", ";"]:
            self.i += 1
        token = self.text[start : self.i].strip()
        if required and not token:
            raise ValueError(f"Missing name at position {self.i}")
        return token or None

    def _parse_length(self) -> float:
        self._consume_whitespace()
        if self._peek() != ":":
            return 0.0
        self.i += 1
        self._consume_whitespace()
        start = self.i
        while self.i < len(self.text) and self.text[self.i] not in [",", ")", ";"]:
            self.i += 1
        raw = self.text[start : self.i].strip()
        if not raw:
            raise ValueError("Missing branch length after ':'")
        try:
            length = float(raw)
        except ValueError as exc:
            raise ValueError(f"Invalid branch length: {raw}") from exc
        if length < 0:
            raise ValueError("Branch lengths must be non-negative")
        return length

    def _peek(self) -> str:
        if self.i >= len(self.text):
            return ""
        return self.text[self.i]

    def _consume_whitespace(self) -> None:
        while self.i < len(self.text) and self.text[self.i].isspace():
            self.i += 1


def read_newick(path: str | Path) -> PhyloTree:
    tree_path = Path(path)
    if not tree_path.exists():
        raise FileNotFoundError(f"Tree not found: {tree_path}")
    parser = _NewickParser(tree_path.read_text(encoding="utf-8"))
    tree = PhyloTree(root=parser.parse())

    names = tree.leaf_names()
    if len(names) != len(set(names)):
        raise ValueError("Duplicate taxa in tree leaves")
    return tree


def tree_summary(tree: PhyloTree) -> dict[str, float | int]:
    lengths = tree.branch_lengths()
    leaves = tree.leaf_names()
    if not lengths:
        lengths = [0.0]
    non_zero = [x for x in lengths if x > 0]
    return {
        "n_taxa": len(leaves),
        "n_branches": len(lengths),
        "min_branch_length": min(lengths),
        "max_branch_length": max(lengths),
        "mean_branch_length": sum(lengths) / len(lengths),
        "min_nonzero_branch_length": min(non_zero) if non_zero else 0.0,
    }
