from __future__ import annotations

from pathlib import Path
from typing import List, Optional

DEFAULT_BOOK_FILENAMES = ("book.yaml", "book.yml", "iac.book.yaml", "iac.book.yml")


def parse_book_dirs(env_value: Optional[str]) -> List[Path]:
    dirs: List[Path] = []
    if env_value:
        for part in env_value.split(":"):
            part = part.strip()
            if part:
                dirs.append(Path(part).expanduser())
    dirs.extend(
        [
            Path("./books"),
            Path("./.iac/books"),
            Path("~/.iac/books").expanduser(),
            Path("."),
        ]
    )

    out: List[Path] = []
    seen = set()
    for d in dirs:
        p = d.resolve()
        if str(p) in seen:
            continue
        seen.add(str(p))
        out.append(p)
    return out


def book_id_for_path(path: Path) -> str:
    """
    Derive a stable "book id" used in refs:
      - /x/foo.yaml -> foo
      - /x/foo.yml  -> foo
      - /x/foo/book.yaml -> foo
      - /x/foo/iac.book.yaml -> foo
    """
    path = path.resolve()
    name = path.name.lower()
    if name in DEFAULT_BOOK_FILENAMES:
        # directory-style book
        return path.parent.name
    # file-style book
    if path.suffix.lower() in (".yaml", ".yml"):
        return path.stem
    return path.stem


def list_books(book_dirs: List[Path]) -> List[Path]:
    """
    Find books:
      - *.yaml/*.yml files
      - */book.yaml, */iac.book.yaml (one level deep)
    """
    found: List[Path] = []
    for d in book_dirs:
        if not d.exists() or not d.is_dir():
            continue

        # file-style books
        for ext in ("*.yaml", "*.yml"):
            for p in d.glob(ext):
                if p.is_file():
                    found.append(p.resolve())

        # directory-style books (one level deep)
        for sub in d.iterdir():
            if not sub.is_dir():
                continue
            for fn in DEFAULT_BOOK_FILENAMES:
                candidate = sub / fn
                if candidate.exists() and candidate.is_file():
                    found.append(candidate.resolve())
                    break

    # unique stable sort by derived id then path
    uniq = {str(p): p for p in found}
    books = list(uniq.values())
    return sorted(books, key=lambda p: (book_id_for_path(p).lower(), str(p).lower()))


def resolve_book(book_ref: str, book_dirs: List[Path]) -> Path:
    """
    Resolve a book reference:
      - If book_ref is an existing file -> use it
      - If book_ref is an existing directory -> look for book.yaml etc inside
      - Else search book_dirs for:
          * <book_ref>.yaml / .yml
          * <book_ref>/book.yaml (and other DEFAULT_BOOK_FILENAMES)
    """
    p = Path(book_ref).expanduser()

    # Direct file path
    if p.exists() and p.is_file():
        return p.resolve()

    # Direct directory path
    if p.exists() and p.is_dir():
        for fn in DEFAULT_BOOK_FILENAMES:
            candidate = p / fn
            if candidate.exists() and candidate.is_file():
                return candidate.resolve()
        raise FileNotFoundError(
            f"Book directory found but no book.yaml inside: {book_ref}"
        )

    # Search by name in dirs
    candidates = [book_ref, f"{book_ref}.yaml", f"{book_ref}.yml"]
    for d in book_dirs:
        # file-style
        for c in candidates:
            q = d / c
            if q.exists() and q.is_file():
                return q.resolve()

        # directory-style: d/book_ref/book.yaml
        sub = d / book_ref
        if sub.exists() and sub.is_dir():
            for fn in DEFAULT_BOOK_FILENAMES:
                candidate = sub / fn
                if candidate.exists() and candidate.is_file():
                    return candidate.resolve()

    raise FileNotFoundError(f"Book not found: {book_ref}")


def split_book_step(ref: str) -> tuple[str, str]:
    if "@" not in ref:
        raise ValueError("step reference must be in the form 'book@step'")
    book, step = ref.split("@", 1)
    book = book.strip()
    step = step.strip()
    if not book or not step:
        raise ValueError("step reference must be in the form 'book@step'")
    return book, step
