from __future__ import annotations

import os
import re
import tomllib
from functools import lru_cache
from pathlib import Path
import frontmatter
from loguru import logger

def slug_to_title(s: str, abbreviations=None) -> str:
    abbreviations = abbreviations or []
    abbrev_set = {str(word).strip().lower() for word in abbreviations if str(word).strip()}
    words = s.replace('-', ' ').replace('_', ' ').split()
    titled = []
    for word in words:
        lowered = word.lower()
        if lowered in abbrev_set:
            titled.append(word.upper())
        elif word.isupper():
            titled.append(word)
        else:
            titled.append(word[0].upper() + word[1:])
    return ' '.join(titled)

def _strip_inline_markdown(text: str) -> str:
    cleaned = text or ""
    cleaned = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', cleaned)
    cleaned = re.sub(r'\[([^\]]+)\]\[[^\]]*\]', r'\1', cleaned)
    cleaned = re.sub(r'`([^`]+)`', r'\1', cleaned)
    cleaned = re.sub(r'\*\*([^*]+)\*\*', r'\1', cleaned)
    cleaned = re.sub(r'__([^_]+)__', r'\1', cleaned)
    cleaned = re.sub(r'\*([^*]+)\*', r'\1', cleaned)
    cleaned = re.sub(r'_([^_]+)_', r'\1', cleaned)
    cleaned = re.sub(r'~~([^~]+)~~', r'\1', cleaned)
    return cleaned

def _plain_text_from_html(text: str) -> str:
    import html
    cleaned = re.sub(r'<[^>]+>', '', text or "")
    return html.unescape(cleaned)

def text_to_anchor(text: str) -> str:
    """Convert text to anchor slug"""
    cleaned = _strip_inline_markdown(text)
    cleaned = _plain_text_from_html(cleaned)
    return re.sub(r'[^\w\s-]', '', cleaned.lower()).replace(' ', '-')

def _unique_anchor(base: str, counts: dict[str, int]) -> str:
    if not base:
        base = "section"
    current = counts.get(base, 0) + 1
    counts[base] = current
    return base if current == 1 else f"{base}-{current}"

_frontmatter_cache: dict[str, tuple[float, tuple[dict, str]]] = {}

def should_exclude_dir(name: str, excluded: set[str]) -> bool:
    """Exclude exact matches plus common derived names like .venv.bak."""
    if name in excluded:
        return True
    prefixes = (".venv", "venv", "node_modules", ".git", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", "dist", "build", ".cache")
    return any(name == prefix or name.startswith(prefix + ".") for prefix in prefixes)

def parse_frontmatter(file_path: str | Path):
    """Parse frontmatter from a markdown file with caching"""
    import time
    start_time = time.time()

    file_path = Path(file_path)
    cache_key = str(file_path)
    try:
        mtime = file_path.stat().st_mtime
    except OSError:
        return {}, ""

    if cache_key in _frontmatter_cache:
        cached_mtime, cached_data = _frontmatter_cache[cache_key]
        if cached_mtime == mtime:
            elapsed = (time.time() - start_time) * 1000
            logger.debug(f"[DEBUG] parse_frontmatter CACHE HIT for {file_path.name} ({elapsed:.2f}ms)")
            return cached_data

    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            text = f.read()
            if not text.startswith(('---\n', '---\r\n', '+++\n', '+++\r\n')):
                result = ({}, text)
                _frontmatter_cache[cache_key] = (mtime, result)
                elapsed = (time.time() - start_time) * 1000
                logger.debug(f"[DEBUG] parse_frontmatter SKIP NO FRONTMATTER {file_path.name} ({elapsed:.2f}ms)")
                return result
            post = frontmatter.loads(text)
            result = (post.metadata, post.content)
            _frontmatter_cache[cache_key] = (mtime, result)
            elapsed = (time.time() - start_time) * 1000
            logger.debug(f"[DEBUG] parse_frontmatter READ FILE {file_path.name} ({elapsed:.2f}ms)")
            return result
    except UnicodeDecodeError as e:
        logger.warning("Skipping non-UTF8 markdown file {}: {}", file_path, e)
        result = ({}, "")
        _frontmatter_cache[cache_key] = (mtime, result)
        return result
    except Exception as e:
        logger.warning("Error parsing frontmatter from {}: {}", file_path, e)
        try:
            text = file_path.read_text(encoding="utf-8", errors="replace")
        except OSError:
            return {}, ""
        result = ({}, text)
        _frontmatter_cache[cache_key] = (mtime, result)
        return result

def resolve_markdown_title(file_path: str | Path, abbreviations=None) -> tuple[str, str]:
    """Get effective title and body, preferring frontmatter title, then first H1, then slug."""
    metadata, raw_content = parse_frontmatter(file_path)
    file_path = Path(file_path)
    explicit_title = metadata.get("title")
    if explicit_title:
        return explicit_title, raw_content

    match = re.match(r"^\s*#\s+(.+?)\s*$\n?", raw_content or "", re.MULTILINE)
    if match:
        title = _plain_text_from_html(_strip_inline_markdown(match.group(1).strip()))
        body = (raw_content[:match.start()] + raw_content[match.end():]).lstrip("\n")
        return title, body

    return slug_to_title(file_path.stem, abbreviations=abbreviations), raw_content

def get_post_title(file_path: str | Path, abbreviations=None) -> str:
    """Get post title from frontmatter or filename"""
    title, _ = resolve_markdown_title(file_path, abbreviations=abbreviations)
    return title

def iter_visible_files(root: Path, suffixes: tuple[str, ...], include_hidden: bool = False):
    """Yield files while pruning hidden and excluded directories before descent."""
    from .config import get_config

    root = root.resolve()
    excluded = set(get_config().get_reload_excludes())
    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = [
            name for name in dirnames
            if (include_hidden or not name.startswith(".")) and not should_exclude_dir(name, excluded)
        ]
        for filename in filenames:
            if not include_hidden and filename.startswith("."):
                continue
            if not filename.endswith(suffixes):
                continue
            yield Path(dirpath) / filename

@lru_cache(maxsize=128)
def _cached_vyasa_config(path_str: str, mtime: float):
    path = Path(path_str)
    try:
        with path.open("rb") as f:
            return tomllib.load(f)
    except Exception:
        return {}

def _normalize_vyasa_config(parsed):
    config = {
        "order": [],
        "sort": "name_asc",
        "folders_first": True,
        "folders_always_first": False,
        "layout_max_width": None,
        "abbreviations": None,
        "ignore": [],
        "include": [],
    }
    if not isinstance(parsed, dict):
        return config

    order = parsed.get("order")
    if order is not None:
        if isinstance(order, (list, tuple)):
            config["order"] = [str(item).strip() for item in order if str(item).strip()]
        else:
            config["order"] = []

    sort = parsed.get("sort")
    if isinstance(sort, str) and sort in ("name_asc", "name_desc", "mtime_asc", "mtime_desc"):
        config["sort"] = sort

    folders_first = parsed.get("folders_first")
    if isinstance(folders_first, bool):
        config["folders_first"] = folders_first
    elif isinstance(folders_first, str):
        lowered = folders_first.lower()
        if lowered in ("true", "false"):
            config["folders_first"] = lowered == "true"

    folders_always_first = parsed.get("folders_always_first")
    if isinstance(folders_always_first, bool):
        config["folders_always_first"] = folders_always_first
    elif isinstance(folders_always_first, str):
        lowered = folders_always_first.lower()
        if lowered in ("true", "false"):
            config["folders_always_first"] = lowered == "true"

    for key in ("layout_max_width",):
        value = parsed.get(key)
        if isinstance(value, (int, float)):
            value = str(value)
        if isinstance(value, str):
            value = value.strip()
            config[key] = value if value else None

    abbreviations = parsed.get("abbreviations")
    if isinstance(abbreviations, (list, tuple, set)):
        config["abbreviations"] = [str(item).strip() for item in abbreviations if str(item).strip()]
    elif isinstance(abbreviations, str):
        parts = [part.strip() for part in abbreviations.split(",")]
        config["abbreviations"] = [part for part in parts if part]

    ignore = parsed.get("ignore")
    if isinstance(ignore, (list, tuple, set)):
        config["ignore"] = [str(item).strip() for item in ignore if str(item).strip()]
    elif isinstance(ignore, str):
        parts = [part.strip() for part in ignore.split(",")]
        config["ignore"] = [part for part in parts if part]

    include = parsed.get("include")
    if isinstance(include, (list, tuple, set)):
        config["include"] = [str(item).strip() for item in include if str(item).strip()]
    elif isinstance(include, str):
        parts = [part.strip() for part in include.split(",")]
        config["include"] = [part for part in parts if part]

    return config

def _effective_abbreviations(root: Path, folder: Path | None = None):
    root_config = get_vyasa_config(root)
    root_abbrevs = root_config.get("abbreviations") or []
    if folder is None or folder == root:
        return root_abbrevs
    folder_config = get_vyasa_config(folder)
    folder_abbrevs = folder_config.get("abbreviations")
    return folder_abbrevs if folder_abbrevs is not None else root_abbrevs

def _effective_ignore_list(root: Path, folder: Path | None = None):
    """Get the effective ignore list for a folder (inherits from root)."""
    root_config = get_vyasa_config(root)
    root_ignore = root_config.get("ignore") or []
    if folder is None or folder == root:
        return root_ignore
    folder_config = get_vyasa_config(folder)
    folder_ignore = folder_config.get("ignore")
    return folder_ignore if folder_ignore is not None else root_ignore

def _effective_include_list(root: Path, folder: Path | None = None):
    """Get the effective include list for a folder (inherits from root)."""
    root_config = get_vyasa_config(root)
    root_include = root_config.get("include") or []
    if folder is None or folder == root:
        return root_include
    folder_config = get_vyasa_config(folder)
    folder_include = folder_config.get("include")
    return folder_include if folder_include is not None else root_include

def _should_include_folder(folder_name: str, include_list: list, ignore_list: list) -> bool:
    """Check if a folder should be included based on include/ignore lists.
    
    Logic:
    - If in ignore list: exclude
    - If include list is empty: include (no whitelist defined)
    - If include list is defined: only include if in the list
    """
    if folder_name in ignore_list:
        return False
    if not include_list:
        return True
    return folder_name in include_list

def get_vyasa_config(folder: Path):
    vyasa_path = folder / ".vyasa"
    if not vyasa_path.exists():
        return _normalize_vyasa_config({})
    try:
        mtime = vyasa_path.stat().st_mtime
    except OSError:
        return _normalize_vyasa_config({})
    parsed = _cached_vyasa_config(str(vyasa_path), mtime)
    config = _normalize_vyasa_config(parsed)
    logger.debug("[DEBUG] .vyasa config for {}: order={} sort={} folders_first={}", folder, config.get("order"), config.get("sort"), config.get("folders_first"))
    return config

def order_vyasa_entries(entries, config):
    if not entries:
        return []

    order_list = [name.strip().rstrip("/") for name in config.get("order", []) if str(name).strip()]
    if not order_list:
        sorted_entries = _sort_vyasa_entries(entries, config.get("sort"), config.get("folders_first", True))
        if config.get("folders_always_first"):
            sorted_entries = _group_folders_first(sorted_entries)
        logger.debug("[DEBUG] .vyasa order empty; sorted entries: {}", [item.name for item in sorted_entries])
        return sorted_entries

    exact_map = {}
    stem_map = {}
    for item in entries:
        exact_map.setdefault(item.name, item)
        if item.suffix == ".md":
            stem_map.setdefault(item.stem, item)

    ordered = []
    used = set()
    for name in order_list:
        if name in exact_map:
            item = exact_map[name]
        elif name in stem_map:
            item = stem_map[name]
        else:
            item = None
        if item and item not in used:
            ordered.append(item)
            used.add(item)

    remaining = [item for item in entries if item not in used]
    remaining_sorted = _sort_vyasa_entries(
        remaining,
        config.get("sort"),
        config.get("folders_first", True)
    )
    combined = ordered + remaining_sorted
    if config.get("folders_always_first"):
        combined = _group_folders_first(combined)
    logger.debug("[DEBUG] .vyasa ordered={} remaining={}", [item.name for item in ordered], [item.name for item in remaining_sorted])
    return combined

def _group_folders_first(entries):
    folders = [item for item in entries if item.is_dir()]
    files = [item for item in entries if not item.is_dir()]
    return folders + files

def _sort_vyasa_entries(entries, sort_method, folders_first):
    method = sort_method or "name_asc"
    reverse = method.endswith("desc")
    by_mtime = method.startswith("mtime")

    def sort_key(item):
        if by_mtime:
            try:
                return item.stat().st_mtime
            except OSError:
                return 0
        return item.name.lower()

    if folders_first:
        folders = [item for item in entries if item.is_dir()]
        files = [item for item in entries if not item.is_dir()]
        folders_sorted = sorted(folders, key=sort_key, reverse=reverse)
        files_sorted = sorted(files, key=sort_key, reverse=reverse)
        return folders_sorted + files_sorted

    return sorted(entries, key=sort_key, reverse=reverse)

def list_vyasa_posts(root: Path, include_hidden: bool = False) -> list[dict]:
    """List all posts in the blog root (md + pdf)."""
    root = root.resolve()
    root_parts = len(root.parts)
    posts: list[dict] = []
    abbreviations = _effective_abbreviations(root)

    for path in sorted(root.rglob("*")):
        if not path.is_file():
            continue
        rel_parts = path.parts[root_parts:]
        if not rel_parts:
            continue
        if not include_hidden and any(part.startswith(".") for part in rel_parts):
            continue
        if path.suffix.lower() not in {".md", ".pdf"}:
            continue

        rel = Path(*rel_parts)
        slug = rel.with_suffix("").as_posix()
        if path.suffix.lower() == ".md":
            title = get_post_title(path, abbreviations=abbreviations)
            kind = "md"
        else:
            title = slug_to_title(rel.stem, abbreviations=abbreviations)
            kind = "pdf"

        posts.append(
            {
                "path": slug,
                "title": title,
                "type": kind,
            }
        )

    return posts

def list_vyasa_entries(root: Path, relative: str = ".", include_hidden: bool = False) -> dict:
    """List immediate entries (folders + md/pdf files) under a relative path."""
    root = root.resolve()
    target = (root / relative).resolve()
    if target != root and root not in target.parents:
        return {"error": "Path escapes blog root"}
    if not target.exists() or not target.is_dir():
        return {"error": "Folder not found"}

    abbreviations = _effective_abbreviations(root, target)
    entries: list[dict] = []
    for item in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
        if not include_hidden and item.name.startswith("."):
            continue
        if item.is_dir():
            entries.append({"type": "folder", "path": item.relative_to(root).as_posix()})
            continue
        if item.suffix.lower() not in {".md", ".pdf"}:
            continue
        rel = item.relative_to(root)
        slug = rel.with_suffix("").as_posix()
        if item.suffix.lower() == ".md":
            title = get_post_title(item, abbreviations=abbreviations)
            kind = "md"
        else:
            title = slug_to_title(rel.stem, abbreviations=abbreviations)
            kind = "pdf"
        entries.append({"type": kind, "path": slug, "title": title})

    return {"path": target.relative_to(root).as_posix(), "entries": entries}

def find_folder_note_file(folder: Path) -> Path | None:
    """Return the preferred folder note file (index.md, readme.md, or foldername.md)."""
    try:
        folder_name = folder.name.lower()
        index_file = None
        readme_file = None
        named_file = None
        for item in folder.iterdir():
            if not item.is_file() or item.suffix.lower() != ".md":
                continue
            stem = item.stem.lower()
            if stem == "index":
                index_file = item
            elif stem == "readme":
                readme_file = item
            elif stem == folder_name:
                named_file = item
        return index_file or readme_file or named_file
    except OSError:
        return None
