"""
Feature detection and management endpoints for SLTK API.

Handles feature discovery, scanning, and connection to datasets.
"""

from __future__ import annotations

import logging
import os
import threading
import uuid
from datetime import datetime
from pathlib import Path

from fastapi import APIRouter, BackgroundTasks, HTTPException, Query

from sltk.api.dependencies import validate_path
from sltk.api.models import (
    ConnectFeaturesRequest,
    DetectedFeatures,
    ExtractFeaturesRequest,
    ExtractionPlanModel,
    ExtractionStepModel,
    FeatureAvailabilityModel,
    FeatureAvailabilityResponseModel,
    FeatureConnectionConfigModel,
    FeatureExtractionResponseModel,
    FeatureScanJobModel,
    FeatureSourceModel,
    FeatureSummaryModel,
    NamingPatternDetectionModel,
    ScanFeaturesRequest,
)

router = APIRouter(tags=["features"])
logger = logging.getLogger(__name__)


def _get_feature_pools() -> list[str]:
    """Get feature pool paths from environment or defaults."""
    from sltk.config import _get_feature_pool_root

    feature_pool_root = _get_feature_pool_root()
    return [
        str(feature_pool_root / "features2"),
        str(feature_pool_root / "features"),
    ]


# Computed at runtime to respect environment variables
FEATURE_POOLS = _get_feature_pools()

# In-memory scan jobs storage
_feature_scan_jobs: dict[str, dict] = {}
_feature_scan_jobs_lock = threading.Lock()


# ============================================================================
# Feature Detection
# ============================================================================


@router.post("/api/features/detect")
async def detect_features(video_paths: list[str], feature_root: str | None = None):
    """
    Auto-detect extracted features for given videos.

    Looks in standard feature pools or specified root for matching features.
    """
    search_roots = [Path(feature_root)] if feature_root else [Path(p) for p in FEATURE_POOLS]

    results = []

    for vpath in video_paths:
        video_path = Path(vpath)
        video_stem = video_path.stem
        video_name = video_path.name

        detected = DetectedFeatures(
            video_name=video_name,
            video_path=str(video_path),
        )

        # Search each feature root for matching features
        for root in search_roots:
            if not root.exists():
                continue

            # Common naming patterns to search
            patterns = [
                # Direct match by video stem
                (root / f"{video_stem}_mediapipe.h5", "mediapipe"),
                (root / f"{video_stem}_wilor.h5", "wilor"),
                (root / f"{video_stem}_nlf.h5", "nlf"),
                (root / f"{video_stem}_smpl.h5", "smpl"),
                (root / f"{video_stem}.eaf", "segments"),
                # Subdirectory patterns
                (root / "poses" / f"{video_stem}_mediapipe.h5", "mediapipe"),
                (root / "poses" / f"{video_stem}_wilor.h5", "wilor"),
                (root / "poses" / f"{video_stem}_nlf.h5", "nlf"),
                (root / "poses" / f"{video_stem}_smpl.h5", "smpl"),
                (root / "segments" / f"{video_stem}.eaf", "segments"),
                (root / "features" / "angles" / f"{video_stem}.npy", "angles"),
                (root / "features" / "hamer" / f"{video_stem}.npy", "hamer"),
                # BSLCP-style paths with video stem as directory
                (root / video_stem / "mediapipe.h5", "mediapipe"),
                (root / video_stem / "wilor.h5", "wilor"),
                (root / video_stem / "nlf.h5", "nlf"),
                (root / video_stem / "smpl.h5", "smpl"),
                (root / video_stem / "segments.eaf", "segments"),
            ]

            for path, feature_type in patterns:
                if path.exists():
                    setattr(detected, feature_type, str(path))

        results.append(detected)

    return {
        "videos_checked": len(video_paths),
        "features": results,
    }


@router.get("/api/features/scan")
async def scan_feature_directory(root: str):
    """
    Scan a directory for extracted features.

    Returns a mapping of video names to their feature files.
    """
    # Security: Validate path is within allowed directories
    root_path = validate_path(root, must_exist=True)
    if not root_path.is_dir():
        raise HTTPException(400, f"Not a directory: {root}")

    features_by_video: dict[str, dict[str, str]] = {}

    # Feature file patterns
    patterns = {
        "_mediapipe.h5": "mediapipe",
        "_wilor.h5": "wilor",
        "_nlf.h5": "nlf",
        "_smpl.h5": "smpl",
        ".eaf": "segments",
    }

    def handle_error(err: OSError):
        pass  # Skip directories we can't access

    for dirpath, dirnames, filenames in os.walk(root_path, onerror=handle_error):
        for filename in filenames:
            filepath = Path(dirpath) / filename

            for suffix, feature_type in patterns.items():
                if filename.lower().endswith(suffix):
                    # Extract video stem
                    video_stem = filename[: -len(suffix)] if suffix != ".eaf" else filename[:-4]

                    if video_stem not in features_by_video:
                        features_by_video[video_stem] = {}

                    features_by_video[video_stem][feature_type] = str(filepath)
                    break

    return {
        "root": str(root_path),
        "video_count": len(features_by_video),
        "features": features_by_video,
    }


@router.post("/api/features/detect-patterns")
async def detect_naming_patterns(
    path: str = Query(..., description="Path to feature directory"),
) -> list[NamingPatternDetectionModel]:
    """
    Detect naming patterns in a feature directory.

    Scans the directory and attempts to identify common naming conventions.
    """
    from sltk.data.feature_discovery import detect_naming_pattern

    validated_path = validate_path(path)

    if not validated_path.is_dir():
        raise HTTPException(400, f"Path is not a directory: {path}")

    patterns = detect_naming_pattern(validated_path)

    return [
        NamingPatternDetectionModel(
            pattern=str(p["pattern"]),
            example=str(p["example"]),
            count=int(p["count"]),
        )
        for p in patterns
    ]


# ============================================================================
# Dataset Feature Connection Endpoints
# ============================================================================


@router.post("/api/datasets/{name}/features/connect", response_model=FeatureConnectionConfigModel)
async def connect_features(name: str, request: ConnectFeaturesRequest):
    """
    Configure feature sources for a dataset.

    Connects one or more feature directories to a dataset,
    specifying the naming pattern for each source.
    """
    from sltk.data.feature_manager import FeatureSource, get_feature_manager

    # Validate all source paths exist and are within allowed roots
    for source in request.sources:
        source_path = validate_path(source.path, must_exist=True)
        if not source_path.is_dir():
            raise HTTPException(400, f"Feature source path is not a directory: {source.path}")

    # Connect features
    manager = get_feature_manager()
    sources = [
        FeatureSource(
            path=s.path,
            feature_types=s.feature_types,
            naming_pattern=s.naming_pattern,
        )
        for s in request.sources
    ]

    config = manager.connect(name, sources)

    logger.info(f"Connected {len(sources)} feature sources to dataset '{name}'")

    return FeatureConnectionConfigModel(
        dataset_name=config.dataset_name,
        feature_sources=[
            FeatureSourceModel(
                path=s.path,
                feature_types=s.feature_types,
                naming_pattern=s.naming_pattern,
            )
            for s in config.feature_sources
        ],
        last_scan=config.last_scan,
    )


@router.get("/api/datasets/{name}/features/config")
async def get_feature_config(name: str) -> FeatureConnectionConfigModel | None:
    """Get the current feature connection config for a dataset."""
    from sltk.data.feature_manager import get_feature_manager

    manager = get_feature_manager()
    config = manager.get_config(name)

    if config is None:
        return None

    return FeatureConnectionConfigModel(
        dataset_name=config.dataset_name,
        feature_sources=[
            FeatureSourceModel(
                path=s.path,
                feature_types=s.feature_types,
                naming_pattern=s.naming_pattern,
            )
            for s in config.feature_sources
        ],
        last_scan=config.last_scan,
    )


@router.post("/api/datasets/{name}/features/scan", response_model=FeatureScanJobModel)
async def scan_features_endpoint(
    name: str,
    request: ScanFeaturesRequest,
    background_tasks: BackgroundTasks,
) -> FeatureScanJobModel:
    """
    Scan for feature availability across samples.

    This runs as a background job for large datasets.
    Returns a job_id to track progress.
    """
    from sltk.data.feature_manager import get_feature_manager

    manager = get_feature_manager()
    config = manager.get_config(name)

    if config is None:
        raise HTTPException(
            400, f"No feature configuration found for dataset: {name}. Connect features first."
        )

    # Get sample IDs
    sample_ids = request.sample_ids
    if sample_ids is None:
        # Try to get sample IDs from dataset
        try:
            from sltk.data.datasets import get_dataset

            dataset = get_dataset(name)
            sample_ids = list(dataset.sample_ids)
        except Exception as e:
            raise HTTPException(
                400,
                f"Could not get sample IDs for dataset '{name}'. Provide sample_ids in request. Error: {e}",
            )

    # Create scan job
    job_id = str(uuid.uuid4())
    with _feature_scan_jobs_lock:
        _feature_scan_jobs[job_id] = {
            "job_id": job_id,
            "dataset_name": name,
            "status": "pending",
            "progress": 0,
            "total_samples": len(sample_ids),
            "scanned_samples": 0,
            "error": None,
        }

    # Run scan in background
    def run_scan():
        try:
            with _feature_scan_jobs_lock:
                _feature_scan_jobs[job_id]["status"] = "running"
            results = manager.scan(name, sample_ids, request.feature_types)
            with _feature_scan_jobs_lock:
                _feature_scan_jobs[job_id]["status"] = "completed"
                _feature_scan_jobs[job_id]["scanned_samples"] = len(results)
                _feature_scan_jobs[job_id]["progress"] = 100
        except Exception as e:
            with _feature_scan_jobs_lock:
                _feature_scan_jobs[job_id]["status"] = "failed"
                _feature_scan_jobs[job_id]["error"] = str(e)

    background_tasks.add_task(run_scan)

    logger.info(f"Started feature scan for dataset '{name}' with {len(sample_ids)} samples")

    with _feature_scan_jobs_lock:
        job = _feature_scan_jobs[job_id]
        return FeatureScanJobModel(**job)


@router.get("/api/datasets/{name}/features/scan/{job_id}", response_model=FeatureScanJobModel)
async def get_scan_status(name: str, job_id: str) -> FeatureScanJobModel:
    """Get the status of a feature scan job."""
    with _feature_scan_jobs_lock:
        if job_id not in _feature_scan_jobs:
            raise HTTPException(404, f"Scan job not found: {job_id}")
        return FeatureScanJobModel(**_feature_scan_jobs[job_id])


@router.get(
    "/api/datasets/{name}/features/availability", response_model=FeatureAvailabilityResponseModel
)
async def get_feature_availability(
    name: str,
    offset: int = Query(0, ge=0),
    limit: int = Query(100, ge=1, le=1000),
) -> FeatureAvailabilityResponseModel:
    """
    Get feature availability matrix (paginated).

    Returns availability for each sample in the cached scan results.
    """
    from sltk.data.feature_manager import get_feature_manager

    manager = get_feature_manager()
    cache = manager._load_cache(name)

    if cache is None:
        return FeatureAvailabilityResponseModel(
            dataset_name=name,
            samples=[],
            total=0,
            offset=offset,
            limit=limit,
            error="No scan data available. Run scan first.",
        )

    # Paginate
    all_sample_ids = sorted(cache.keys())
    page_ids = all_sample_ids[offset : offset + limit]

    samples = [
        FeatureAvailabilityModel(
            sample_id=sid,
            available=cache[sid].available,
            missing=cache[sid].missing,
        ).model_dump()
        for sid in page_ids
    ]

    return FeatureAvailabilityResponseModel(
        dataset_name=name,
        samples=samples,
        total=len(all_sample_ids),
        offset=offset,
        limit=limit,
    )


@router.get("/api/datasets/{name}/features/summary", response_model=FeatureSummaryModel)
async def get_feature_summary(name: str):
    """Get quick summary of feature availability from cache."""
    from sltk.data.feature_manager import get_feature_manager

    manager = get_feature_manager()
    summary = manager.get_summary(name)

    if summary is None:
        return FeatureSummaryModel(
            dataset_name=name,
            total_samples=0,
            feature_counts={},
            last_scan=None,
        )

    return FeatureSummaryModel(
        dataset_name=name,
        total_samples=summary.total_samples,
        feature_counts=summary.feature_counts,
        last_scan=summary.last_scan,
    )


@router.get("/api/datasets/{name}/samples/{sample_id}/features")
async def get_sample_features(name: str, sample_id: str) -> dict[str, str]:
    """Get all feature file paths for a specific sample."""
    from sltk.data.feature_manager import get_feature_manager

    manager = get_feature_manager()
    return manager.get_sample_features(name, sample_id)


@router.get("/api/datasets/{name}/features/extract/plan", response_model=ExtractionPlanModel)
async def get_extraction_plan(
    name: str,
    target_features: str = Query(..., description="Comma-separated list of target features"),
):
    """
    Preview extraction plan (dry-run).

    Shows dependency resolution and step ordering without executing.
    """
    from sltk.data.feature_manager import get_feature_manager

    targets = [t.strip() for t in target_features.split(",")]

    manager = get_feature_manager()

    try:
        plan = manager.plan_extraction(name, targets)
    except ValueError as e:
        raise HTTPException(400, str(e))

    return ExtractionPlanModel(
        target_features=plan.target_features,
        steps=[
            ExtractionStepModel(
                order=s.order,
                feature_type=s.feature_type,
                sample_count=len(s.sample_ids),
                sample_ids=s.sample_ids[:100],  # Limit for response
                depends_on=s.depends_on,
                is_extraction=s.is_extraction,
            )
            for s in plan.steps
        ],
        total_samples=plan.total_samples,
        total_extractions=plan.total_extractions,
    )


@router.post("/api/datasets/{name}/features/extract", response_model=FeatureExtractionResponseModel)
async def extract_features(
    name: str,
    request: ExtractFeaturesRequest,
    background_tasks: BackgroundTasks,
) -> FeatureExtractionResponseModel:
    """
    Extract only missing features (dependency-aware).

    Creates an extraction job that processes features in dependency order.
    """
    from sltk.api.models import ExtractionJob
    from sltk.api.routers.extraction import _jobs_lock, jobs
    from sltk.data.feature_manager import get_feature_manager

    # Validate output path
    output_path = validate_path(request.output_path, must_exist=False, allow_creation=True)

    manager = get_feature_manager()

    # Get extraction plan
    try:
        plan = manager.plan_extraction(name, request.target_features)
    except ValueError as e:
        raise HTTPException(400, str(e))

    if plan.total_extractions == 0:
        return FeatureExtractionResponseModel(
            status="no_extraction_needed",
            message="All requested features are already available",
        )

    # Create extraction job (reuse existing extraction infrastructure)
    job_id = str(uuid.uuid4())
    job = ExtractionJob(
        job_id=job_id,
        status="pending",
        created_at=datetime.now().isoformat(),
        total_videos=plan.total_extractions,
        processed_videos=0,
        current_frame=0,
        total_frames=0,
        stages=[s.feature_type for s in plan.steps],
        stage_index=0,
        stage_statuses={},
        successful=[],
        failed=[],
        stage_errors={},
        input_path=name,
        output_root=str(output_path),
    )
    with _jobs_lock:
        jobs[job_id] = job

    logger.info(f"Created feature extraction job {job_id} for {plan.total_extractions} extractions")

    # TODO: Actually run extraction in background
    # This would integrate with the existing extraction pipeline

    return FeatureExtractionResponseModel(
        status="created",
        job_id=job_id,
        plan=plan.to_dict(),
    )
