"""Load repos + knowledge folder into text blocks for the Claude system prompt."""

from __future__ import annotations

import logging
from pathlib import Path

logger = logging.getLogger(__name__)

SKIP_DIRS = {
    ".git", "__pycache__", "node_modules", ".venv", "venv",
    "dist", "build", ".next", "target", ".idea", ".gradle",
}
CODE_EXT = {
    ".py", ".java", ".ts", ".tsx", ".js", ".go", ".rs",
    ".kt", ".swift", ".cpp", ".c", ".cs", ".rb", ".scala",
}


def _read_single_repo(path: str, max_chars: int = 20_000) -> str:
    """Read directory tree + key source files from a single repo."""
    root = Path(path).expanduser().resolve()
    if not root.is_dir():
        return ""

    # --- File tree (depth <= 4) ---
    tree_lines: list[str] = []

    def _tree(d: Path, prefix: str = "", depth: int = 0) -> None:
        if depth > 4:
            return
        try:
            children = sorted(d.iterdir(), key=lambda p: (p.is_file(), p.name))
        except PermissionError:
            return
        for i, child in enumerate(children):
            if child.name.startswith(".") or child.name in SKIP_DIRS:
                continue
            connector = "└── " if i == len(children) - 1 else "├── "
            tree_lines.append(prefix + connector + child.name)
            if child.is_dir() and depth < 4 and len(tree_lines) < 200:
                ext = "    " if i == len(children) - 1 else "│   "
                _tree(child, prefix + ext, depth + 1)

    _tree(root)
    sections: list[str] = [
        f"# Codebase: {root.name}\n",
        "## File tree\n```\n" + root.name + "/\n" + "\n".join(tree_lines) + "\n```\n",
    ]
    total = sum(len(s) for s in sections)

    # --- Key doc files ---
    for fname in ("README.md", "README.rst", "package.json", "pyproject.toml", "pom.xml"):
        fpath = root / fname
        if fpath.exists() and total < max_chars:
            try:
                content = fpath.read_text(errors="ignore")[:5_000]
                sec = f"## {fname}\n```\n{content}\n```\n"
                sections.append(sec)
                total += len(sec)
            except Exception:
                pass

    # --- Source files (entry points first, then by size) ---
    source_files = [
        p for p in root.rglob("*")
        if p.suffix in CODE_EXT
        and not any(s in p.parts for s in SKIP_DIRS)
    ]

    def _priority(p: Path) -> tuple:
        if p.stem.lower() in ("main", "app", "index", "server", "api", "application"):
            return (0, p.stat().st_size)
        return (1, p.stat().st_size)

    source_files.sort(key=_priority)

    for fpath in source_files[:40]:
        if total >= max_chars:
            break
        try:
            content = fpath.read_text(errors="ignore")
            rel = fpath.relative_to(root)
            remaining = max_chars - total
            excerpt = content[:min(4_000, remaining)]
            sec = f"## {rel}\n```{fpath.suffix[1:]}\n{excerpt}\n```\n"
            sections.append(sec)
            total += len(sec)
        except Exception:
            pass

    return "\n".join(sections)


def read_repos_context(repos: list[str], max_chars_per_repo: int = 20_000) -> str:
    """Read file tree + key source files from each repo. Returns combined text."""
    if not repos:
        return ""
    parts: list[str] = []
    for repo_path in repos:
        block = _read_single_repo(repo_path, max_chars=max_chars_per_repo)
        if block:
            parts.append(block)
    if not parts:
        return ""
    return "\n\n---\n\n".join(parts)


def read_knowledge_folder(path: str, max_chars: int = 20_000) -> str:
    """Recursively read all .md and .txt files from the knowledge folder."""
    root = Path(path).expanduser().resolve()
    if not root.is_dir():
        return ""

    files = sorted(
        [p for p in root.rglob("*") if p.suffix in (".md", ".txt") and p.is_file()],
        key=lambda p: str(p.relative_to(root)),
    )

    if not files:
        return ""

    sections: list[str] = []
    total = 0

    for fpath in files:
        if total >= max_chars:
            break
        try:
            content = fpath.read_text(errors="ignore")
            rel = fpath.relative_to(root)
            remaining = max_chars - total
            excerpt = content[:remaining]
            sec = f"## {rel}\n\n{excerpt}\n\n---"
            sections.append(sec)
            total += len(sec)
        except Exception:
            pass

    if not sections:
        return ""

    return f"# Knowledge Base ({len(sections)} documents)\n\n" + "\n\n".join(sections)


def build_full_context(repos: list[str], knowledge_path: str) -> str:
    """Combine repos + knowledge into a single context block for system prompt."""
    parts: list[str] = []

    repos_ctx = read_repos_context(repos)
    if repos_ctx:
        parts.append(repos_ctx)
        logger.info("Loaded context from %d repo(s) (%d chars)", len(repos), len(repos_ctx))

    if knowledge_path:
        knowledge_ctx = read_knowledge_folder(knowledge_path)
        if knowledge_ctx:
            parts.append(knowledge_ctx)
            logger.info("Loaded knowledge folder (%d chars)", len(knowledge_ctx))

    if not parts:
        return ""

    return "\n\n---\n\n".join(parts)


# ---------------------------------------------------------------------------
# Repos workspace scanning
# ---------------------------------------------------------------------------


def get_repos_from_workspace(
    workspace_path: str, ignored: list[str] | None = None
) -> list[dict]:
    """Scan a workspace folder and return all subdirectories as available repos.

    Returns: [{name, path, sanitized_name}] sorted by name.
    Only includes direct children that are directories (repos).
    Ignored repo names (folder names) are excluded.
    """
    root = Path(workspace_path).expanduser().resolve()
    if not root.is_dir():
        return []

    ignored_set = set(ignored or [])
    repos = []
    try:
        for child in sorted(root.iterdir(), key=lambda p: p.name.lower()):
            if not child.is_dir() or child.name.startswith("."):
                continue
            if child.name in ignored_set:
                continue
            sanitized = child.name.replace("_", " ").replace("-", " ").title()
            repos.append({
                "name": child.name,
                "path": str(child),
                "sanitized_name": sanitized,
            })
    except PermissionError:
        pass
    return repos


# ---------------------------------------------------------------------------
# Selective loading (per-meeting context selector)
# ---------------------------------------------------------------------------


def get_knowledge_tree(
    root_path: str, ignored: list[str] | None = None
) -> list[dict]:
    """Return nested folder structure of the knowledge base for the GUI tree widget.

    Returns: [{name, path, children, doc_count}, ...]
    Only includes directories that contain .md/.txt files (directly or in descendants).
    Ignored folder names are excluded.
    """
    root = Path(root_path).expanduser().resolve()
    if not root.is_dir():
        return []

    ignored_set = set(ignored or [])

    def _scan(directory: Path, depth: int = 0) -> list[dict]:
        result = []
        try:
            children = sorted(directory.iterdir(), key=lambda p: p.name)
        except PermissionError:
            return []
        for child in children:
            if not child.is_dir() or child.name.startswith(".") or child.name in SKIP_DIRS:
                continue
            if depth == 0 and child.name in ignored_set:
                continue
            docs = [
                p for p in child.rglob("*")
                if p.suffix in (".md", ".txt") and p.is_file()
            ]
            if not docs:
                continue
            result.append({
                "name": child.name,
                "path": str(child.relative_to(root)),
                "children": _scan(child, depth + 1),
                "doc_count": len(docs),
            })
        return result

    return _scan(root)


def read_knowledge_folders(
    root_path: str,
    folder_relpaths: list[str],
    max_chars: int = 20_000,
) -> str:
    """Read .md/.txt files only from selected subfolders of the knowledge base."""
    root = Path(root_path).expanduser().resolve()
    if not root.is_dir() or not folder_relpaths:
        return ""

    files: list[Path] = []
    for relpath in folder_relpaths:
        folder = root / relpath
        if not folder.is_dir():
            continue
        files.extend(
            p for p in folder.rglob("*")
            if p.suffix in (".md", ".txt") and p.is_file()
        )

    files = sorted(set(files), key=lambda p: str(p.relative_to(root)))
    if not files:
        return ""

    sections: list[str] = []
    total = 0

    for fpath in files:
        if total >= max_chars:
            break
        try:
            content = fpath.read_text(errors="ignore")
            rel = fpath.relative_to(root)
            remaining = max_chars - total
            excerpt = content[:remaining]
            sec = f"## {rel}\n\n{excerpt}\n\n---"
            sections.append(sec)
            total += len(sec)
        except Exception:
            pass

    if not sections:
        return ""

    return f"# Knowledge Base ({len(sections)} documents)\n\n" + "\n\n".join(sections)


def read_repo_readme(repo_path: str, max_chars: int = 2000) -> str:
    """Read only the root README from a repo — used for the low-level index."""
    root = Path(repo_path).expanduser().resolve()
    if not root.is_dir():
        return f"### {root.name}\n\n(directory not found)\n"
    for name in ("README.md", "README.rst", "README.txt", "readme.md", "Readme.md"):
        candidate = root / name
        if candidate.is_file():
            try:
                content = candidate.read_text(encoding="utf-8", errors="ignore")[:max_chars]
                return f"### {root.name}\n\n{content}\n"
            except Exception:
                pass
    return f"### {root.name}\n\n(no README found)\n"


def read_knowledge_folder_readme(folder_path: str, max_chars: int = 2000) -> str:
    """Read only the README (or first .md) from a knowledge subfolder — for the index."""
    folder = Path(folder_path).expanduser().resolve()
    if not folder.is_dir():
        return f"### {folder.name}\n\n(directory not found)\n"
    display = folder.name.replace("_", " ").replace("-", " ").title()
    # Include actual folder name in brackets so Claude returns the correct path
    heading = f"### {display} [folder: {folder.name}]"
    # Try README.md first
    for name in ("README.md", "readme.md", "Readme.md"):
        candidate = folder / name
        if candidate.is_file():
            try:
                content = candidate.read_text(encoding="utf-8", errors="ignore")[:max_chars]
                return f"{heading}\n\n{content}\n"
            except Exception:
                pass
    # Fall back to first .md file directly in this folder (not recursive)
    for candidate in sorted(folder.iterdir()):
        if candidate.is_file() and candidate.suffix.lower() == ".md":
            try:
                content = candidate.read_text(encoding="utf-8", errors="ignore")[:max_chars]
                return f"{heading}\n\n{content}\n"
            except Exception:
                pass
    return f"{heading}\n\n(no README found)\n"


def get_repos_from_workspaces(
    workspace_paths: list[str], ignored: list[str] | None = None
) -> list[dict]:
    """Scan multiple workspace folders and return all repos pooled together."""
    seen: dict[str, dict] = {}
    for ws in workspace_paths:
        for repo in get_repos_from_workspace(ws, ignored=ignored):
            seen[repo["name"]] = repo  # last wins on name collision
    return sorted(seen.values(), key=lambda r: r["name"].lower())


def get_knowledge_trees(
    paths: list[str], ignored: list[str] | None = None
) -> list[dict]:
    """Scan multiple knowledge folders and return merged flat folder list."""
    seen: dict[str, dict] = {}
    for kp in paths:
        for node in get_knowledge_tree(kp, ignored=ignored):
            if node["name"] not in seen:
                seen[node["name"]] = node
    return sorted(seen.values(), key=lambda n: n["name"].lower())


def read_knowledge_folders_multi(
    knowledge_paths: list[str],
    folder_relpaths: list[str],
    max_chars: int = 20_000,
) -> str:
    """Read selected knowledge folders from multiple knowledge roots."""
    # Try each knowledge root and collect from the first that contains the folder
    for kp in knowledge_paths:
        ctx = read_knowledge_folders(kp, folder_relpaths, max_chars=max_chars)
        if ctx:
            return ctx
    return ""


def build_low_level_index(
    repos_workspace: str | list[str],
    knowledge_path: str | list[str],
    ignored_repos: list[str] | None = None,
    ignored_knowledge: list[str] | None = None,
) -> str:
    """Build a lightweight index of all repos + knowledge folders (READMEs only).

    This is the Level-1 context: always loaded at meeting start so Claude knows
    what is available. Full content is loaded selectively via build_selective_context().
    Ignored repos/knowledge folders are excluded from the index entirely.
    """
    parts: list[str] = []

    ws_list = [repos_workspace] if isinstance(repos_workspace, str) else repos_workspace
    kp_list = [knowledge_path] if isinstance(knowledge_path, str) else knowledge_path
    ws_list = [w for w in ws_list if w]
    kp_list = [k for k in kp_list if k]

    if ws_list:
        repos = get_repos_from_workspaces(ws_list, ignored=ignored_repos)
        if repos:
            repo_sections = [read_repo_readme(r["path"]) for r in repos]
            parts.append(
                "## Available Repositories (index — README only)\n\n"
                + "\n".join(repo_sections)
            )
            logger.debug("Low-level index: %d repos", len(repos))

    if kp_list:
        all_nodes: list[dict] = []
        for kp in kp_list:
            tree = get_knowledge_tree(kp, ignored=ignored_knowledge)
            for node in tree:
                node["_root"] = kp
                all_nodes.append(node)
        if all_nodes:
            kb_sections = [
                read_knowledge_folder_readme(
                    str(Path(n["_root"]).expanduser().resolve() / n["path"])
                )
                for n in all_nodes
            ]
            parts.append(
                "## Available Knowledge Base Topics (index — README only)\n\n"
                + "\n".join(kb_sections)
            )
            logger.debug("Low-level index: %d knowledge folders", len(all_nodes))

    if not parts:
        return ""

    index = "\n\n---\n\n".join(parts)
    logger.info("Low-level index built: %d chars", len(index))
    return index


def build_selective_context(
    repos: list[str],
    knowledge_root: str,
    knowledge_folders: list[str],
) -> str:
    """Build context from selected repos and selected knowledge subfolders only."""
    parts: list[str] = []

    if repos:
        repos_ctx = read_repos_context(repos)
        if repos_ctx:
            parts.append(repos_ctx)
            logger.info("Selective: loaded %d repo(s) (%d chars)", len(repos), len(repos_ctx))

    if knowledge_root and knowledge_folders:
        knowledge_ctx = read_knowledge_folders(knowledge_root, knowledge_folders)
        if knowledge_ctx:
            parts.append(knowledge_ctx)
            logger.info("Selective: loaded %d knowledge folder(s) (%d chars)", len(knowledge_folders), len(knowledge_ctx))

    if not parts:
        return ""

    return "\n\n---\n\n".join(parts)
