"""Validation stage: fail-fast checks on parsed data."""

from pathlib import Path

from .cache import resolve_with_cache
from .errors import ValidationError, ValidationIssue
from .parser import _read_json, resolve_missing_videos
from .models import (
    ProjectConfig,
    SlideDefinition,
    VideoSource,
    SLIDE_LAYOUTS,
    CAMERA_PRESETS,
)


def validate_project(
    project_path: Path,
    manuscript_markers: list[str],
    config: ProjectConfig,
    slides: dict[str, SlideDefinition],
    videos: dict[str, VideoSource],
    videos_dir: Path,
    malformed_markers: list[tuple[int, str]] = None,
    audio: dict = None,
) -> list[ValidationIssue]:
    """
    Validate all parsed project data. Raises ValidationError if any issues found.
    Returns a list of warnings (non-fatal issues).

    Checks:
    - All slide markers in manuscript exist in slides.json
    - All slide images exist on disk
    - All video files exist on disk
    - Background video exists (if specified)
    - Slide types are valid
    - No malformed markers in manuscript
    - All audio: markers in manuscript exist in audio.json
    """
    issues: list[ValidationIssue] = []
    warnings: list[ValidationIssue] = []

    # Collect video IDs actually referenced in the manuscript (for file-existence checks)
    _VIDEO_PREFIXES = {
        "video:":  6,
        "vft:":    4, "vfb:":    4, "vfm:":    4,
        "vf2t:":   5, "vf2b:":   5, "vf2m:":   5,
        "vst:":    4, "vsb:":    4, "vsm:":    4,
        "vftp:":   5, "vfbp:":   5, "vfmp:":   5,
        "vf2tp:":  6, "vf2bp:":  6, "vf2mp:":  6,
        "vstp:":   5, "vsbp:":   5, "vsmp:":   5,
    }
    referenced_video_ids: set[str] = set()
    for marker in manuscript_markers:
        prefix = next((p for p in _VIDEO_PREFIXES if marker.startswith(p)), None)
        if prefix is not None:
            referenced_video_ids.add(marker[_VIDEO_PREFIXES[prefix]:].lower())
        elif marker.startswith("narration:"):
            referenced_video_ids.add(marker[10:].lower())

    # Check for malformed markers first (these are likely typos)
    if malformed_markers:
        for line_num, marker_text in malformed_markers:
            issues.append(
                ValidationIssue(
                    f"Malformed marker: {marker_text}",
                    project_path / "manuscript.txt",
                    line_num,
                )
            )

    # Check all manuscript markers have corresponding slides or videos
    for marker in manuscript_markers:
        # Skip camera effect markers (Zoom0, TiltLeft, Reset, etc.)
        if marker in CAMERA_PRESETS:
            continue
        # Skip audio markers (start with 'A' followed by audio id, e.g., Awoosh)
        if marker.startswith("A") and len(marker) > 1 and marker[1:].isalnum():
            continue
        # Skip audio: prefix markers (e.g., audio:woosh)
        if marker.startswith("audio:"):
            continue
        # Validate video trigger markers — both legacy [video:xxx] and
        # shorthand [vft:xxx] / [vfb:xxx] / [vst:xxx] / [vsb:xxx].
        matched_prefix = next(
            (p for p in _VIDEO_PREFIXES if marker.startswith(p)), None
        )
        if matched_prefix is not None:
            video_id = marker[_VIDEO_PREFIXES[matched_prefix] :].lower()
            if video_id not in videos:
                hint = ""
                if "." in video_id:
                    base_name = video_id.rsplit(".", 1)[0]
                    if base_name in videos:
                        hint = f" (Did you mean [{matched_prefix}{base_name}]? Don't include file extensions in markers)"
                warnings.append(
                    ValidationIssue(
                        f"Video marker [{marker}] referenced in manuscript but '{video_id}' not defined in videos.json{hint} — using PlaceholderVideo instead",
                        project_path / "manuscript.txt",
                    )
                )
            else:
                vs = videos[video_id]
                if not vs.cutout or vs.cutout not in config.cutouts:
                    warnings.append(
                        ValidationIssue(
                            f"[{marker}] video '{video_id}' has no valid cutout in videos.json — "
                            f"run 'gnommo import' to project values, or set cutout manually.",
                            project_path / "manuscript.txt",
                        )
                    )
            continue

        # Validate narration trigger markers (narration:xxx) - continuous videos
        if marker.startswith("narration:"):
            video_id = marker[10:].lower()  # Remove 'narration:' prefix
            if video_id not in videos:
                warnings.append(
                    ValidationIssue(
                        f"Narration marker [{marker}] referenced in manuscript but '{video_id}' not defined in videos.json — using PlaceholderVideo instead",
                        project_path / "manuscript.txt",
                    )
                )
            else:
                vs = videos[video_id]
                if not vs.cutout or vs.cutout not in config.cutouts:
                    warnings.append(
                        ValidationIssue(
                            f"[{marker}] video '{video_id}' has no valid cutout in videos.json — "
                            f"run 'gnommo import' to project values, or set cutout manually.",
                            project_path / "manuscript.txt",
                        )
                    )
            continue

        # Segment markers are structural annotations, not slide references
        if marker.startswith("segment:"):
            continue

        # Bare narrator cues (teleprompter hints, not pipeline markers)
        if marker in ("pause", "stop"):
            continue

        # Unknown namespaced markers (e.g. [background:xxx]) — not supported, ignore with warning
        if ":" in marker:
            warnings.append(
                ValidationIssue(
                    f"Unknown marker type [{marker}] — ignoring (no support for '{marker.split(':', 1)[0]}:' markers)",
                    project_path / "manuscript.txt",
                )
            )
            continue

        if marker not in slides:
            issues.append(
                ValidationIssue(
                    f"Slide marker [{marker}] referenced in manuscript but not defined in slides.json",
                    project_path / "manuscript.txt",
                )
            )

    # Check all slide images exist
    # Slides are in the same directory as the slides.json file.
    # Lowercase the configured path so capital-cased project names (e.g.
    # "media/slides/Video2/slides.json") resolve on case-sensitive filesystems.
    slides_json_path = project_path / config.slides_path.lower()
    slides_dir = slides_json_path.parent

    for slide_id, slide_def in slides.items():
        image_path = slides_dir / slide_def.image
        image_path, _ = resolve_with_cache(image_path, project_path)
        if not image_path.exists():
            issues.append(
                ValidationIssue(
                    f"Slide image not found: {slide_def.image}", slides_json_path
                )
            )

        # Check slide type is valid
        if slide_def.type not in SLIDE_LAYOUTS:
            issues.append(
                ValidationIssue(
                    f"Unknown slide type '{slide_def.type}' for slide {slide_id}. "
                    f"Valid types: {list(SLIDE_LAYOUTS.keys())}",
                    project_path / "slides.json",
                )
            )

    # Check all video files exist (paths relative to videos_dir or shared_assets)
    videos_json_path = project_path / config.videos_path

    # Find shared_assets directory
    shared_assets_dir = None
    if (project_path / "shared_assets").exists():
        shared_assets_dir = project_path / "shared_assets"
    elif (project_path.parent / "shared_assets").exists():
        shared_assets_dir = project_path.parent / "shared_assets"

    for video_id, video_source in videos.items():
        # Only check files for videos actually used in this manuscript
        if video_id not in referenced_video_ids:
            continue

        # Determine base directory based on is_shared flag
        if video_source.is_shared:
            if shared_assets_dir:
                base_dir = shared_assets_dir
            else:
                issues.append(
                    ValidationIssue(
                        f"Video '{video_id}' has is_shared=true but shared_assets directory not found",
                        videos_json_path,
                    )
                )
                continue
        else:
            base_dir = videos_dir

        video_path = base_dir / video_source.source_file
        video_path, _ = resolve_with_cache(video_path, project_path)
        if not video_path.exists():
            sf = video_source.source_file
            hint = (
                " — run 'gnommo pexels' to download"
                if sf.startswith("pexels/")
                else " — falling back to PlaceholderVideo"
            )
            warnings.append(
                ValidationIssue(
                    f"Video file not found: {sf}{hint}",
                    videos_json_path,
                )
            )

        # Check preprocessed output exists if filters are defined
        if video_source.filter and video_source.output_file:
            output_path = base_dir / video_source.output_file
            output_path, _ = resolve_with_cache(output_path, project_path)
            if not output_path.exists():
                issues.append(
                    ValidationIssue(
                        f"Preprocessed output not found: {video_source.output_file}. "
                        f"Run with -a preprocess first.",
                        videos_json_path,
                    )
                )

    # Check background exists — must be a handle in shared_assets/videos.json
    bg_handle = config.background
    if bg_handle:
        shared_assets_dir = project_path.parent / "shared_assets"
        videos_json_path_bg = shared_assets_dir / "videos.json"
        if not videos_json_path_bg.exists():
            issues.append(
                ValidationIssue(
                    f"shared_assets/videos.json not found (needed for background handle '{bg_handle}')",
                    project_path / "project.json",
                )
            )
        else:
            bg_videos = _read_json(videos_json_path_bg)
            if bg_handle not in bg_videos:
                issues.append(
                    ValidationIssue(
                        f"Background handle '{bg_handle}' not found in shared_assets/videos.json",
                        project_path / "project.json",
                    )
                )
            else:
                bg_path = shared_assets_dir / bg_videos[bg_handle]["source_file"]
                bg_path, _ = resolve_with_cache(bg_path, project_path)
                if not bg_path.exists():
                    issues.append(
                        ValidationIssue(
                            f"Background file not found: {bg_path} (from handle '{bg_handle}')",
                            project_path / "project.json",
                        )
                    )

    # Check videos.json exists (empty is fine — project may not need triggered videos)
    if not (project_path / config.videos_path).exists():
        issues.append(
            ValidationIssue(
                "videos.json not found — run 'gnommo import' to create it",
                project_path / "videos.json",
            )
        )

    # Check resolution is reasonable
    width, height = config.resolution
    if width < 50 or height < 50:
        issues.append(
            ValidationIssue(
                f"Resolution too small: {width}x{height}", project_path / "project.json"
            )
        )

    if width > 7680 or height > 4320:
        issues.append(
            ValidationIssue(
                f"Resolution too large: {width}x{height} (max 8K)",
                project_path / "project.json",
            )
        )

    # Check FPS is reasonable
    if config.fps < 1 or config.fps > 120:
        issues.append(
            ValidationIssue(
                f"Invalid FPS: {config.fps} (must be 1-120)",
                project_path / "project.json",
            )
        )

    # Check outro videos exist in videos.json or shared_assets
    if config.outro:
        missing_outro = [vid_id for vid_id in config.outro if vid_id not in videos]
        if missing_outro:
            found = resolve_missing_videos(missing_outro, project_path, config)
            still_missing = [vid_id for vid_id in missing_outro if vid_id not in found]
            for vid_id in still_missing:
                warnings.append(
                    ValidationIssue(
                        f"Outro video '{vid_id}' not found in videos.json or shared_assets — will be skipped at render",
                        project_path / "project.json",
                    )
                )

    # Check all audio: markers in manuscript exist in audio.json
    if audio is not None:
        seen_audio: set[str] = set()
        manuscript_path = project_path / "manuscript.txt"
        for marker in manuscript_markers:
            audio_id = None
            if marker.startswith("audio:"):
                audio_id = marker[6:]
            elif marker.startswith("A") and len(marker) > 1 and marker[1:].isalnum():
                audio_id = marker[1:]
            if audio_id and audio_id not in seen_audio:
                seen_audio.add(audio_id)
                if audio_id not in audio:
                    warnings.append(
                        ValidationIssue(
                            f"Audio marker [{marker}] referenced in manuscript but '{audio_id}' not defined in audio.json — will be silent at render",
                            manuscript_path,
                        )
                    )

    # If any issues, raise ValidationError
    if issues:
        raise ValidationError(issues)

    return warnings
