"""Hydrate ContextTierService from linked project resources.

Bridges the gap between the resource registry (files on disk) and
the ACMS context tier (in-memory fragments). Without this, the
ContextTierService starts empty on every CLI process invocation
and the LLM receives zero file context during plan execution.

This module resolves bug #1028.
"""

from __future__ import annotations

import os
import subprocess
from pathlib import Path
from typing import Any

import structlog

from cleveragents.application.services.context_tiers import ContextTierService
from cleveragents.domain.models.acms.tiers import ContextTier, TieredFragment

logger = structlog.get_logger(__name__)

# Maximum file size to index (256 KB). Larger files are skipped.
_MAX_FILE_BYTES = 256 * 1024

# Maximum total bytes to index per project (10 MB).
_MAX_TOTAL_BYTES = 10 * 1024 * 1024

# Directories to always skip
_SKIP_DIRS = frozenset(
    {
        ".git",
        ".hg",
        ".svn",
        "__pycache__",
        "node_modules",
        ".venv",
        "venv",
        ".nox",
        ".tox",
        ".mypy_cache",
        ".pytest_cache",
        ".ruff_cache",
        "dist",
        "build",
        ".eggs",
        ".cleveragents",
    }
)

# Binary file extensions to skip
_BINARY_EXTS = frozenset(
    {
        ".pyc",
        ".pyo",
        ".so",
        ".o",
        ".a",
        ".dll",
        ".exe",
        ".png",
        ".jpg",
        ".jpeg",
        ".gif",
        ".bmp",
        ".ico",
        ".pdf",
        ".zip",
        ".tar",
        ".gz",
        ".bz2",
        ".xz",
        ".whl",
        ".egg",
        ".db",
        ".sqlite",
        ".sqlite3",
    }
)


def hydrate_tiers_from_project(
    tier_service: ContextTierService,
    project_name: str,
    resource_id: str,
    resource_location: str,
    resource_type: str = "git-checkout",
) -> int:
    """Read files from a resource and store as TieredFragments.

    Args:
        tier_service: The ContextTierService to populate.
        project_name: Namespaced project name (e.g. ``local/my-project``).
        resource_id: ULID of the resource.
        resource_location: Filesystem path to the resource root.
        resource_type: Resource type name (affects file listing strategy).

    Returns:
        Number of fragments stored.
    """
    if not resource_location or not os.path.isdir(resource_location):
        logger.warning(
            "context_hydrator.skip_missing_location",
            project=project_name,
            resource_id=resource_id,
            location=resource_location,
        )
        return 0

    files = _list_files(resource_location, resource_type)
    stored = 0
    total_bytes = 0

    for rel_path in files:
        abs_path = os.path.join(resource_location, rel_path)

        try:
            size = os.path.getsize(abs_path)
        except OSError:
            continue

        if size > _MAX_FILE_BYTES:
            continue
        if total_bytes + size > _MAX_TOTAL_BYTES:
            break

        try:
            content = Path(abs_path).read_text(encoding="utf-8")
        except (UnicodeDecodeError, OSError):
            continue

        fragment = TieredFragment(
            fragment_id=f"{resource_id}:{rel_path}",
            content=content,
            tier=ContextTier.HOT,
            resource_id=resource_id,
            project_name=project_name,
            token_count=len(content) // 4,
            metadata={
                "path": rel_path,
                "detail_depth": "1",
                "relevance_score": "0.5",
            },
        )

        try:
            tier_service.store(fragment)
            stored += 1
            total_bytes += size
        except Exception:
            logger.debug(
                "context_hydrator.store_failed",
                fragment_id=fragment.fragment_id,
                exc_info=True,
            )

    logger.info(
        "context_hydrator.hydrated",
        project=project_name,
        resource_id=resource_id,
        fragments_stored=stored,
        total_bytes=total_bytes,
    )
    return stored


def hydrate_tiers_for_plan(
    tier_service: ContextTierService,
    project_names: list[str],
    project_repository: Any,
    resource_registry: Any,
) -> int:
    """Hydrate tiers for all projects linked to a plan.

    Args:
        tier_service: The ContextTierService to populate.
        project_names: List of namespaced project names.
        project_repository: NamespacedProjectRepository instance.
        resource_registry: ResourceRegistryService instance.

    Returns:
        Total number of fragments stored across all projects.
    """
    total = 0

    for project_name in project_names:
        try:
            project = project_repository.get(project_name)
        except Exception:
            logger.debug(
                "context_hydrator.project_not_found",
                project=project_name,
            )
            continue

        if project is None:
            continue

        linked = getattr(project, "linked_resources", [])
        if not linked:
            logger.debug(
                "context_hydrator.no_linked_resources",
                project=project_name,
            )
            continue

        for lr in linked:
            try:
                resource = resource_registry.show_resource(lr.resource_id)
            except Exception:
                logger.debug(
                    "context_hydrator.resource_not_found",
                    resource_id=lr.resource_id,
                )
                continue

            if not resource.location:
                continue

            count = hydrate_tiers_from_project(
                tier_service=tier_service,
                project_name=project_name,
                resource_id=resource.resource_id,
                resource_location=resource.location,
                resource_type=resource.resource_type_name,
            )
            total += count

    return total


def _list_files(
    root: str,
    resource_type: str,
) -> list[str]:
    """List files in a resource directory.

    For git-checkout resources, uses ``git ls-files`` for tracked files.
    Falls back to ``os.walk`` for other resource types.
    """
    if resource_type in ("git-checkout", "git"):
        files = _git_ls_files(root)
        if files is not None:
            return files

    return _walk_files(root)


def _git_ls_files(root: str) -> list[str] | None:
    """List tracked files via ``git ls-files``."""
    try:
        result = subprocess.run(
            ["git", "ls-files", "--cached", "--others", "--exclude-standard"],
            cwd=root,
            capture_output=True,
            text=True,
            timeout=30,
            check=False,
        )
        if result.returncode != 0:
            return None
        files = []
        for line in result.stdout.strip().split("\n"):
            line = line.strip()
            if not line:
                continue
            ext = os.path.splitext(line)[1].lower()
            if ext in _BINARY_EXTS:
                continue
            files.append(line)
        return files
    except (subprocess.TimeoutExpired, OSError):
        return None


def _walk_files(root: str) -> list[str]:
    """Walk directory tree, skipping hidden/binary/large files."""
    files: list[str] = []
    for dirpath, dirnames, filenames in os.walk(root):
        # Filter out skip dirs in-place
        dirnames[:] = [
            d
            for d in dirnames
            if d not in _SKIP_DIRS
            and not d.startswith(".")
            and not d.endswith(".egg-info")
        ]

        for fname in filenames:
            if fname.startswith("."):
                continue
            ext = os.path.splitext(fname)[1].lower()
            if ext in _BINARY_EXTS:
                continue
            rel = os.path.relpath(os.path.join(dirpath, fname), root)
            files.append(rel)

    return files
