"""
Project registry management for Lattice.

Manages projects.toml registration and project lifecycle.

Reference: RFC-002 §6.1
"""

# @invar:allow shell_result: Path getters, no I/O needed
# @invar:allow shell_pure_logic: Path computation helpers
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path

from returns.result import Failure, Result, Success

from lattice.core.types.evidence import ExcludedProject, ProjectRegistration
from lattice.shell.config import resolve_global_dir


# @invar:allow shell_result: Path getter, no I/O
# @invar:allow shell_pure_logic: Pure path computation
def get_projects_path() -> Path:
    """Get path to projects.toml (in ~/.config/lattice/).

    Returns:
        Path to projects.toml.
    """
    result = resolve_global_dir()
    if isinstance(result, Success):
        return result.unwrap() / "projects.toml"
    return Path.home() / ".config" / "lattice" / "projects.toml"


# @invar:allow shell_result: Path getter, no I/O
# @invar:allow shell_pure_logic: Pure path computation
def get_projects_dir() -> Path:
    """Get path to ~/.config/lattice/ directory.

    Returns:
        Path to global lattice directory.
    """
    result = resolve_global_dir()
    if isinstance(result, Success):
        return result.unwrap()
    return Path.home() / ".config" / "lattice"


@dataclass(frozen=True)
class ProjectInfo:
    """Project info with status."""

    name: str
    path: str
    registered_at: str
    exists: bool


def load_projects() -> Result[list[ProjectRegistration], str]:
    """Load all registered projects from projects.toml.

    Returns:
        Success(list of ProjectRegistration) or Failure(error).
    """
    import tomllib

    projects_path = get_projects_path()
    if not projects_path.exists():
        return Success([])

    try:
        with open(projects_path, "rb") as f:
            data = tomllib.load(f)

        projects = []
        for name, info in data.get("projects", {}).items():
            projects.append(
                ProjectRegistration(
                    path=info.get("path", ""),
                    name=name,
                    registered_at=info.get("registered_at", ""),
                )
            )
        return Success(projects)
    except Exception as e:
        return Failure(f"Failed to load projects: {e}")


def save_projects(projects: list[ProjectRegistration]) -> Result[None, str]:
    """Save projects to projects.toml.

    Returns:
        Success(None) or Failure(error).
    """
    projects_path = get_projects_path()
    projects_path.parent.mkdir(parents=True, exist_ok=True)

    try:
        lines = ["# Lattice Project Registry\n", "\n", "[projects]\n"]
        for proj in projects:
            lines.append(f'\n[projects."{proj.name}"]\n')
            lines.append(f'path = "{proj.path}"\n')
            lines.append(f'registered_at = "{proj.registered_at}"\n')

        projects_path.write_text("".join(lines), encoding="utf-8")
        return Success(None)
    except Exception as e:
        return Failure(f"Failed to save projects: {e}")


# @invar:allow shell_result: Pure name generation, no I/O
# @shell_pure_logic: Pure name generation logic
def generate_unique_project_name(
    base_name: str, parent_dir_name: str, existing_names: list[str]
) -> str:
    """Generate a unique project name, handling collisions.

    If base_name already exists in existing_names, appends parent_dir_name.

    Args:
        base_name: Preferred project name (usually directory name).
        parent_dir_name: Parent directory name to append on collision.
        existing_names: List of already-registered project names.

    Returns:
        Unique project name (single-pass collision resolution only).

    Note:
        If both base_name and the disambiguated name are taken, the disambiguated
        name is returned regardless. The caller's register_project will then fail
        with "already registered" and the auto-registration is silently skipped.

    >>> generate_unique_project_name("myproject", "projects", [])
    'myproject'
    >>> generate_unique_project_name("myproject", "projects", ["myproject"])
    'projects-myproject'
    >>> generate_unique_project_name("myproject", "projects", ["other", "myproject"])
    'projects-myproject'
    >>> # Double collision: returns disambiguated name even if also taken
    >>> generate_unique_project_name("myproject", "projects", ["myproject", "projects-myproject"])
    'projects-myproject'
    """
    if base_name in existing_names:
        return f"{parent_dir_name}-{base_name}"
    return base_name


# @shell_complexity: Path validation + collision check + duplicate detection + registry save
def register_project(
    path: str, name: str | None = None
) -> Result[ProjectRegistration, str]:
    """Register a project in projects.toml.

    Args:
        path: Path to project directory.
        name: Optional project name (default: directory name).

    Returns:
        Success(ProjectRegistration) or Failure(error).
    """
    proj_path = Path(path).resolve()
    if not proj_path.exists():
        return Failure(f"Path does not exist: {proj_path}")

    if not proj_path.is_dir():
        return Failure(f"Path is not a directory: {proj_path}")

    # Generate unique name, handling collisions
    project_name = name or proj_path.name
    all_projects_result = load_projects()
    if isinstance(all_projects_result, Success):
        existing_names = [p.name for p in all_projects_result.unwrap()]
        project_name = generate_unique_project_name(
            project_name, proj_path.parent.name, existing_names
        )

    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

    registration = ProjectRegistration(
        path=str(proj_path),
        name=project_name,
        registered_at=timestamp,
    )

    # Load existing projects
    load_result = load_projects()
    if isinstance(load_result, Failure):
        projects = []
    else:
        projects = load_result.unwrap()

    # Check for duplicate
    for p in projects:
        if p.name == project_name:
            return Failure(f"Project '{project_name}' already registered")
        if p.path == str(proj_path):
            return Failure(f"Path already registered as '{p.name}'")

    # Add new project
    projects.append(registration)

    # Save
    save_result = save_projects(projects)
    if isinstance(save_result, Failure):
        return save_result

    return Success(registration)


def unregister_project(name: str) -> Result[None, str]:
    """Remove a project from the registry.

    Args:
        name: Project name to remove.

    Returns:
        Success(None) or Failure(error).
    """
    load_result = load_projects()
    if isinstance(load_result, Failure):
        return load_result

    projects = load_result.unwrap()
    original_count = len(projects)
    projects = [p for p in projects if p.name != name]

    if len(projects) == original_count:
        return Failure(f"Project '{name}' not found")

    return save_projects(projects)


def list_projects() -> Result[list[ProjectInfo], str]:
    """List all registered projects with status.

    Returns:
        Success(list of ProjectInfo) or Failure(error).
    """
    load_result = load_projects()
    if isinstance(load_result, Failure):
        return load_result

    projects = load_result.unwrap()
    infos = []

    for proj in projects:
        path = Path(proj.path)
        infos.append(
            ProjectInfo(
                name=proj.name,
                path=proj.path,
                registered_at=proj.registered_at,
                exists=path.exists(),
            )
        )

    return Success(infos)


# @shell_complexity: Path existence check + .lattice/ check + multi-branch status reporting
def verify_projects() -> Result[list[tuple[str, bool, str]], str]:
    """Verify all registered project paths exist.

    Returns:
        Success(list of (name, exists, message)) or Failure(error).
    """
    load_result = load_projects()
    if isinstance(load_result, Failure):
        return load_result

    projects = load_result.unwrap()
    results = []

    for proj in projects:
        path = Path(proj.path)
        if path.exists():
            if (path / ".lattice").exists():
                results.append((proj.name, True, "OK"))
            else:
                results.append((proj.name, False, "Missing .lattice/"))
        else:
            results.append((proj.name, False, "Path not found"))

    return Success(results)


def load_excluded() -> Result[list[ExcludedProject], str]:
    """Load all excluded projects from projects.toml.

    Returns:
        Success(list of ExcludedProject) or Failure(error).

    >>> # When file doesn't exist
    >>> load_excluded()  # doctest: +SKIP
    Success([])
    """
    import tomllib

    projects_path = get_projects_path()
    if not projects_path.exists():
        return Success([])

    try:
        with open(projects_path, "rb") as f:
            data = tomllib.load(f)

        excluded = []
        for item in data.get("excluded", []):
            excluded.append(
                ExcludedProject(
                    path=item.get("path", ""),
                    excluded_at=item.get("excluded_at", ""),
                    reason=item.get("reason", ""),
                )
            )
        return Success(excluded)
    except Exception as e:
        return Failure(f"Failed to load excluded: {e}")


# @invar:allow shell_result: Pure TOML formatting, no I/O
# @shell_pure_logic: Pure TOML formatting helpers
def _format_projects_section(projects: list[ProjectRegistration]) -> list[str]:
    """Format [projects] section for TOML file.

    >>> from lattice.core.types.evidence import ProjectRegistration
    >>> proj = ProjectRegistration(path="/a", name="a", registered_at="2026-01-01T00:00:00Z")
    >>> lines = _format_projects_section([proj])
    >>> lines[0]
    '[projects]\\n'
    """
    if not projects:
        return []

    lines = ["[projects]\n"]
    for proj in projects:
        safe_name = proj.name.replace("\\", "\\\\").replace('"', '\\"')
        safe_path = proj.path.replace("\\", "\\\\").replace('"', '\\"')
        lines.append(f'\n[projects."{safe_name}"]\n')
        lines.append(f'path = "{safe_path}"\n')
        lines.append(f'registered_at = "{proj.registered_at}"\n')
    return lines


# @invar:allow shell_result: Pure TOML formatting, no I/O
# @shell_pure_logic: Pure TOML formatting helpers
# @shell_complexity: Conditional reason field + TOML escaping for each excluded entry
def _format_excluded_section(excluded: list[ExcludedProject]) -> list[str]:
    """Format [[excluded]] section for TOML file.

    >>> ex = ExcludedProject(path="/a", excluded_at="2026-01-01T00:00:00Z", reason="test")
    >>> lines = _format_excluded_section([ex])
    >>> lines[0]
    '[[excluded]]\\n'
    """
    if not excluded:
        return []

    lines = []
    for ex in excluded:
        safe_path = ex.path.replace("\\", "\\\\").replace('"', '\\"')
        safe_reason = ex.reason.replace("\\", "\\\\").replace('"', '\\"') if ex.reason else ""
        lines.append("[[excluded]]\n")
        lines.append(f'path = "{safe_path}"\n')
        lines.append(f'excluded_at = "{ex.excluded_at}"\n')
        if safe_reason:
            lines.append(f'reason = "{safe_reason}"\n')
        lines.append("\n")
    return lines


# @shell_complexity: Load existing projects + format two sections + write with error handling
def save_excluded(excluded: list[ExcludedProject]) -> Result[None, str]:
    """Save excluded projects to projects.toml.

    Must preserve existing [projects] section!
    Read existing file, update only the excluded section.

    Returns:
        Success(None) or Failure(error).
    """
    projects_path = get_projects_path()
    projects_path.parent.mkdir(parents=True, exist_ok=True)

    try:
        # Load existing projects if file exists
        existing_projects: list[ProjectRegistration] = []
        if projects_path.exists():
            load_result = load_projects()
            if isinstance(load_result, Success):
                existing_projects = load_result.unwrap()

        # Build file content preserving [projects] section
        lines = ["# Lattice Project Registry\n", "\n"]

        # Write [projects] section
        lines.extend(_format_projects_section(existing_projects))
        if existing_projects:
            lines.append("\n")

        # Write [[excluded]] section
        lines.extend(_format_excluded_section(excluded))

        projects_path.write_text("".join(lines), encoding="utf-8")
        return Success(None)
    except Exception as e:
        return Failure(f"Failed to save excluded: {e}")


def is_excluded(path: str) -> Result[bool, str]:
    """Check if a path is in the excluded list.

    Args:
        path: Path to check (will be resolved to absolute).

    Returns:
        Success(True) if excluded, Success(False) if not.

    >>> is_excluded("/nonexistent/path")  # doctest: +NORMALIZE_WHITESPACE
    <Success: False>
    """
    load_result = load_excluded()
    if isinstance(load_result, Failure):
        return load_result

    excluded = load_result.unwrap()
    resolved_path = str(Path(path).resolve())

    for ex in excluded:
        if ex.path == resolved_path:
            return Success(True)

    return Success(False)


# @shell_complexity: Follows register_project pattern (path validation + registry update)
def exclude_project(path: str, reason: str = "") -> Result[ExcludedProject, str]:
    """Exclude a project from lazy initialization.

    Adds the project to the excluded list in projects.toml.
    If already registered, also removes from [projects] section.

    Args:
        path: Path to project directory to exclude.
        reason: Optional reason for exclusion.

    Returns:
        Success(ExcludedProject) or Failure(error).

    >>> exclude_project("/nonexistent")  # doctest: +SKIP
    Failure("Path does not exist: /nonexistent")
    """
    proj_path = Path(path).resolve()
    if not proj_path.exists():
        return Failure(f"Path does not exist: {proj_path}")

    resolved_path = str(proj_path)

    # Check if already excluded
    check_result = is_excluded(resolved_path)
    if isinstance(check_result, Failure):
        return check_result
    if check_result.unwrap():
        return Failure(f"Path already excluded: {resolved_path}")

    # Create exclusion record
    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    excluded_project = ExcludedProject(
        path=resolved_path,
        excluded_at=timestamp,
        reason=reason,
    )

    # Load existing excluded list
    load_result = load_excluded()
    if isinstance(load_result, Failure):
        excluded_list = []
    else:
        excluded_list = load_result.unwrap()

    # Add new exclusion
    excluded_list.append(excluded_project)

    # Save excluded list
    save_result = save_excluded(excluded_list)
    if isinstance(save_result, Failure):
        return save_result

    # If path is in registered projects, unregister it
    project_result = find_project_by_path(resolved_path)
    if isinstance(project_result, Success):
        unregister_result = unregister_project(project_result.unwrap().name)
        if isinstance(unregister_result, Failure):
            # Log but don't fail - exclusion succeeded
            pass

    return Success(excluded_project)


def include_project(path: str) -> Result[None, str]:
    """Re-include a previously excluded project.

    Removes the project from the excluded list.
    Does NOT auto-register - that happens on next use.

    Args:
        path: Path to project directory to re-include.

    Returns:
        Success(None) or Failure(error).

    >>> include_project("/nonexistent")  # doctest: +NORMALIZE_WHITESPACE
    <Failure: Path is not in excluded list>
    """
    resolved_path = str(Path(path).resolve())

    # Load excluded list
    load_result = load_excluded()
    if isinstance(load_result, Failure):
        return load_result

    excluded_list = load_result.unwrap()
    original_count = len(excluded_list)

    # Remove matching entry
    excluded_list = [ex for ex in excluded_list if ex.path != resolved_path]

    if len(excluded_list) == original_count:
        return Failure("Path is not in excluded list")

    # Save updated list
    return save_excluded(excluded_list)


def find_project_by_path(path: str) -> Result[ProjectRegistration, str]:
    """Find a registered project by its path.

    Args:
        path: Absolute path to search for.

    Returns:
        Success(ProjectRegistration) if found, Failure if not.
    """
    load_result = load_projects()
    if isinstance(load_result, Failure):
        return load_result

    projects = load_result.unwrap()
    for proj in projects:
        if proj.path == path:
            return Success(proj)

    return Failure(f"No project registered at path: {path}")


__all__ = [
    "get_projects_path",
    "get_projects_dir",
    "load_projects",
    "save_projects",
    "register_project",
    "unregister_project",
    "list_projects",
    "verify_projects",
    "ProjectInfo",
    "load_excluded",
    "save_excluded",
    "is_excluded",
    "exclude_project",
    "include_project",
    "find_project_by_path",
    "generate_unique_project_name",
    "_format_projects_section",
    "_format_excluded_section",
]
