"""Campaign discovery — scan filesystem for existing campaigns.

Campaigns live in separate SQLite databases under
~/.unfamiliar/campaigns/<name>-<hash>/. This module scans that directory
and returns metadata for all found campaigns.
"""

import logging
import sqlite3
from datetime import datetime
from pathlib import Path

from unfamiliar.campaign.types import CampaignInfo
from unfamiliar.constants import CampaignStatus

logger = logging.getLogger(__name__)

_DB_FILENAME = "campaign.db"
_REPO_PATH_MARKER = ".repo_path"
# Sentinel for sorting — any datetime is greater than this
_MIN_DATETIME = datetime.min


def discover_campaigns(base_dir: Path | None = None) -> list[CampaignInfo]:
    """Scan the campaigns directory and return metadata for all found campaigns.

    Never raises — returns empty list on missing directory or scan errors.
    Each CampaignInfo includes campaign_dir, repo_path, status, is_orphaned,
    timestamps, and mission counts.

    Args:
        base_dir: Base directory for unfamiliar data. Defaults to ~/.unfamiliar.

    Returns:
        List of CampaignInfo sorted by updated_at descending (most recent first).
        Corrupt or unreadable campaigns are logged and skipped.
    """
    _base = base_dir or (Path.home() / ".unfamiliar")
    campaigns_dir = _base / "campaigns"

    if not campaigns_dir.exists():
        logger.debug("Campaigns directory does not exist: %s", campaigns_dir)
        return []

    if not campaigns_dir.is_dir():
        logger.warning("Campaigns path is not a directory: %s", campaigns_dir)
        return []

    results: list[CampaignInfo] = []

    for entry in campaigns_dir.iterdir():
        if not entry.is_dir():
            continue

        info = _scan_campaign_directory(entry)
        if info is not None:
            results.append(info)

    # Sort by updated_at descending (most recent first), None values at end
    results.sort(key=lambda c: c.updated_at or _MIN_DATETIME, reverse=True)

    return results


def _scan_campaign_directory(campaign_dir: Path) -> CampaignInfo | None:
    """Scan a single campaign directory and extract metadata.

    Returns None on any error (corrupt DB, missing files, etc.).
    Errors are logged but not raised.
    """
    db_path = campaign_dir / _DB_FILENAME
    if not db_path.exists():
        logger.debug("No database in campaign directory: %s", campaign_dir)
        return None

    # Read repo path from marker file
    repo_path = _read_repo_path_marker(campaign_dir)

    # Check if repo is orphaned
    is_orphaned = repo_path is None or not repo_path.exists()

    # Read campaign metadata from database
    try:
        with sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) as conn:
            conn.row_factory = sqlite3.Row
            return _read_campaign_info(conn, campaign_dir, repo_path, is_orphaned)
    except sqlite3.Error as exc:
        logger.warning("Failed to read campaign database %s: %s", db_path, exc)
        return None
    except Exception as exc:  # noqa: BLE001 — catch all to avoid crashing discovery
        logger.warning("Unexpected error reading campaign %s: %s", db_path, exc)
        return None


def _read_repo_path_marker(campaign_dir: Path) -> Path | None:
    """Read the .repo_path marker file if it exists.

    Returns None if the marker doesn't exist or can't be read.
    """
    marker = campaign_dir / _REPO_PATH_MARKER
    if not marker.exists():
        return None

    try:
        path_str = marker.read_text().strip()
        if not path_str:
            return None
        return Path(path_str)
    except OSError as exc:
        logger.debug("Could not read repo path marker %s: %s", marker, exc)
        return None


def _read_campaign_info(
    conn: sqlite3.Connection,
    campaign_dir: Path,
    repo_path: Path | None,
    is_orphaned: bool,
) -> CampaignInfo | None:
    """Read campaign metadata and mission counts from the database.

    Returns None on any database error.
    """
    try:
        cursor = conn.cursor()

        # Read campaign record
        cursor.execute(
            """
            SELECT status, created_at, updated_at
            FROM campaign
            LIMIT 1
            """
        )
        row = cursor.fetchone()
        if row is None:
            logger.debug("No campaign record in database: %s", campaign_dir)
            return None

        status = _parse_campaign_status(row["status"])
        created_at = _parse_datetime(row["created_at"])
        updated_at = _parse_datetime(row["updated_at"])

        # Count missions
        cursor.execute("SELECT COUNT(*) as total FROM mission")
        mission_count = cursor.fetchone()["total"]

        cursor.execute(
            "SELECT COUNT(*) as completed FROM mission WHERE status = ?",
            ("completed",)
        )
        completed_count = cursor.fetchone()["completed"]

        return CampaignInfo(
            campaign_dir=campaign_dir,
            repo_path=repo_path,
            status=status,
            is_orphaned=is_orphaned,
            created_at=created_at,
            updated_at=updated_at,
            mission_count=mission_count,
            completed_count=completed_count,
        )
    except sqlite3.Error as exc:
        logger.warning("Database query error in %s: %s", campaign_dir, exc)
        return None


def _parse_campaign_status(value: str | None) -> CampaignStatus:
    """Parse a campaign status string, defaulting to INITIALIZING."""
    if value is None:
        return CampaignStatus.INITIALIZING

    try:
        return CampaignStatus(value)
    except ValueError:
        logger.debug("Unknown campaign status '%s', defaulting to INITIALIZING", value)
        return CampaignStatus.INITIALIZING


def _parse_datetime(value: str | None) -> datetime | None:
    """Parse an ISO 8601 datetime string.

    Returns None if the value is None or empty.
    """
    if not value:
        return None

    try:
        # SQLite stores timestamps as ISO 8601 strings
        return datetime.fromisoformat(value)
    except ValueError:
        logger.debug("Could not parse datetime '%s'", value)
        return None
