"""Baseline storage for visual regression.

Stores baselines in:
1. Local filesystem (for development)
2. Supabase storage (for CI/production)

Supports:
- Branch-aware baselines (different baselines per branch)
- Viewport-specific baselines (mobile vs desktop)
- Automatic baseline updates on approval
"""
from __future__ import annotations

import hashlib
import json
import logging
import os
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from autodebug.visual.capture import VisualSnapshot
from autodebug.visual.diff import DiffResult, DiffStatus, VisualDiff

logger = logging.getLogger(__name__)


@dataclass
class BaselineInfo:
    """Information about a stored baseline."""

    name: str
    content_hash: str
    created_at: datetime
    updated_at: datetime
    branch: str
    viewport: Optional[str] = None  # e.g., "1920x1080", "mobile"
    approved_by: Optional[str] = None
    metadata: Dict[str, Any] = None

    def __post_init__(self):
        if self.metadata is None:
            self.metadata = {}

    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "content_hash": self.content_hash,
            "created_at": self.created_at.isoformat(),
            "updated_at": self.updated_at.isoformat(),
            "branch": self.branch,
            "viewport": self.viewport,
            "approved_by": self.approved_by,
            "metadata": self.metadata,
        }


@dataclass
class ComparisonResult:
    """Result of comparing a snapshot against baseline."""

    snapshot_name: str
    diff_result: DiffResult
    baseline_info: Optional[BaselineInfo]
    snapshot: VisualSnapshot
    baseline_bytes: Optional[bytes] = None

    @property
    def needs_review(self) -> bool:
        """Check if this needs human review."""
        return self.diff_result.status in (DiffStatus.DIFF, DiffStatus.NEW, DiffStatus.SIZE_MISMATCH)

    @property
    def passed(self) -> bool:
        """Check if comparison passed."""
        return self.diff_result.passed

    def to_dict(self) -> Dict[str, Any]:
        return {
            "snapshot_name": self.snapshot_name,
            "diff": self.diff_result.to_dict(),
            "baseline": self.baseline_info.to_dict() if self.baseline_info else None,
            "snapshot": self.snapshot.to_dict(),
            "needs_review": self.needs_review,
            "passed": self.passed,
        }


class BaselineStorage(ABC):
    """Abstract base for baseline storage."""

    @abstractmethod
    def get_baseline(self, name: str, branch: str, viewport: Optional[str] = None) -> Optional[bytes]:
        """Get baseline image bytes."""
        pass

    @abstractmethod
    def get_baseline_info(self, name: str, branch: str, viewport: Optional[str] = None) -> Optional[BaselineInfo]:
        """Get baseline metadata."""
        pass

    @abstractmethod
    def save_baseline(
        self,
        name: str,
        image: bytes,
        branch: str,
        viewport: Optional[str] = None,
        approved_by: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> BaselineInfo:
        """Save a new baseline."""
        pass

    @abstractmethod
    def delete_baseline(self, name: str, branch: str, viewport: Optional[str] = None) -> bool:
        """Delete a baseline."""
        pass

    @abstractmethod
    def list_baselines(self, branch: Optional[str] = None) -> List[BaselineInfo]:
        """List all baselines."""
        pass

    def compare(
        self,
        snapshot: VisualSnapshot,
        branch: str = "main",
        viewport: Optional[str] = None,
        threshold: float = 0.1,
    ) -> ComparisonResult:
        """Compare snapshot against stored baseline.

        Args:
            snapshot: The snapshot to compare
            branch: Branch name for baseline lookup
            viewport: Viewport identifier
            threshold: Diff threshold

        Returns:
            ComparisonResult
        """
        # Get baseline
        baseline_bytes = self.get_baseline(snapshot.name, branch, viewport)
        baseline_info = self.get_baseline_info(snapshot.name, branch, viewport)

        if baseline_bytes is None:
            # No baseline exists - this is a new snapshot
            return ComparisonResult(
                snapshot_name=snapshot.name,
                diff_result=DiffResult(
                    status=DiffStatus.NEW,
                    diff_percentage=0,
                    diff_pixels=0,
                    total_pixels=0,
                ),
                baseline_info=None,
                snapshot=snapshot,
            )

        # Compare images
        differ = VisualDiff(threshold=threshold)
        diff_result = differ.compare(baseline_bytes, snapshot.screenshot)

        return ComparisonResult(
            snapshot_name=snapshot.name,
            diff_result=diff_result,
            baseline_info=baseline_info,
            snapshot=snapshot,
            baseline_bytes=baseline_bytes,
        )

    def approve(
        self,
        snapshot: VisualSnapshot,
        branch: str = "main",
        viewport: Optional[str] = None,
        approved_by: Optional[str] = None,
    ) -> BaselineInfo:
        """Approve snapshot as new baseline.

        Args:
            snapshot: Snapshot to approve
            branch: Branch to save baseline for
            viewport: Viewport identifier
            approved_by: User who approved

        Returns:
            New BaselineInfo
        """
        return self.save_baseline(
            name=snapshot.name,
            image=snapshot.screenshot,
            branch=branch,
            viewport=viewport,
            approved_by=approved_by,
            metadata=snapshot.metadata,
        )

    def reject(
        self,
        name: str,
        branch: str = "main",
        viewport: Optional[str] = None,
        rejected_by: Optional[str] = None,
        reason: Optional[str] = None,
    ) -> bool:
        """Reject a visual change and preserve the existing baseline (P7-F5).

        Records the rejection decision in the baseline metadata without
        updating the baseline image.  This gives an explicit "no" signal
        compared to silently ignoring a diff.

        Args:
            name: Snapshot name
            branch: Branch of the baseline
            viewport: Viewport identifier
            rejected_by: User who rejected
            reason: Human-readable reason for rejection

        Returns:
            True if the rejection was recorded, False if no baseline exists
        """
        info = self.get_baseline_info(name, branch, viewport)
        if info is None:
            return False

        info.metadata["last_rejection"] = {
            "rejected_by": rejected_by,
            "reason": reason,
            "rejected_at": datetime.now(timezone.utc).isoformat(),
        }
        # Persist the updated metadata by re-saving the existing baseline.
        # The image bytes are not changed -- only the JSON sidecar is updated.
        existing_image = self.get_baseline(name, branch, viewport)
        if existing_image is not None:
            self.save_baseline(
                name=name,
                image=existing_image,
                branch=branch,
                viewport=viewport,
                approved_by=info.approved_by,
                metadata=info.metadata,
            )
        logger.info(f"Rejected visual change: {name} (by {rejected_by}): {reason}")
        return True

    def add_review_comment(
        self,
        name: str,
        branch: str = "main",
        viewport: Optional[str] = None,
        author: Optional[str] = None,
        text: str = "",
    ) -> bool:
        """Add a review comment/annotation to a baseline (P7-F5).

        Args:
            name: Snapshot name
            branch: Branch of the baseline
            viewport: Viewport identifier
            author: Comment author
            text: Comment text

        Returns:
            True if the comment was recorded, False if no baseline exists
        """
        info = self.get_baseline_info(name, branch, viewport)
        if info is None:
            return False

        comments = info.metadata.setdefault("review_comments", [])
        comments.append({
            "author": author,
            "text": text,
            "created_at": datetime.now(timezone.utc).isoformat(),
        })

        existing_image = self.get_baseline(name, branch, viewport)
        if existing_image is not None:
            self.save_baseline(
                name=name,
                image=existing_image,
                branch=branch,
                viewport=viewport,
                approved_by=info.approved_by,
                metadata=info.metadata,
            )
        logger.info(f"Review comment added to {name} by {author}")
        return True


class LocalBaselineStorage(BaselineStorage):
    """Store baselines on local filesystem.

    Good for development and when baselines should be committed to git.

    Directory structure:
        baselines/
            main/
                homepage.png
                homepage.json
                checkout-form.png
                checkout-form.json
            feature-branch/
                ...
    """

    def __init__(self, base_dir: Path = None):
        """Initialize local storage.

        Args:
            base_dir: Base directory for baselines (default: ./baselines)
        """
        self.base_dir = base_dir or Path.cwd() / "baselines"
        self.base_dir.mkdir(parents=True, exist_ok=True)

    def _get_path(self, name: str, branch: str, viewport: Optional[str], ext: str) -> Path:
        """Get path for a baseline file."""
        safe_name = name.replace("/", "_").replace(" ", "_")
        if viewport:
            safe_name = f"{safe_name}_{viewport}"

        branch_dir = self.base_dir / branch.replace("/", "_")
        branch_dir.mkdir(parents=True, exist_ok=True)

        return branch_dir / f"{safe_name}.{ext}"

    def get_baseline(self, name: str, branch: str, viewport: Optional[str] = None) -> Optional[bytes]:
        """Get baseline image bytes."""
        path = self._get_path(name, branch, viewport, "png")

        if not path.exists():
            # Try main branch as fallback
            if branch != "main":
                return self.get_baseline(name, "main", viewport)
            return None

        return path.read_bytes()

    def get_baseline_info(self, name: str, branch: str, viewport: Optional[str] = None) -> Optional[BaselineInfo]:
        """Get baseline metadata."""
        path = self._get_path(name, branch, viewport, "json")

        if not path.exists():
            # Try main branch as fallback
            if branch != "main":
                return self.get_baseline_info(name, "main", viewport)
            return None

        try:
            data = json.loads(path.read_text())
            return BaselineInfo(
                name=data["name"],
                content_hash=data["content_hash"],
                created_at=datetime.fromisoformat(data["created_at"]),
                updated_at=datetime.fromisoformat(data["updated_at"]),
                branch=data["branch"],
                viewport=data.get("viewport"),
                approved_by=data.get("approved_by"),
                metadata=data.get("metadata", {}),
            )
        except Exception as e:
            logger.warning(f"Failed to read baseline info: {e}")
            return None

    def save_baseline(
        self,
        name: str,
        image: bytes,
        branch: str,
        viewport: Optional[str] = None,
        approved_by: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> BaselineInfo:
        """Save a new baseline.

        If a baseline already exists, the previous version is archived into
        a ``_history/`` sub-directory so it can be recovered later (P7-F4).
        The original ``created_at`` timestamp is preserved across updates.
        """
        now = datetime.now(timezone.utc)
        content_hash = hashlib.sha256(image).hexdigest()[:16]

        img_path = self._get_path(name, branch, viewport, "png")
        json_path = self._get_path(name, branch, viewport, "json")

        # P7-F4: Archive the previous baseline before overwriting
        existing_info = self.get_baseline_info(name, branch, viewport)
        original_created_at = now  # default for brand-new baselines
        if existing_info is not None and img_path.exists():
            original_created_at = existing_info.created_at
            history_dir = img_path.parent / "_history"
            history_dir.mkdir(parents=True, exist_ok=True)
            ts = now.strftime("%Y%m%dT%H%M%S")
            archive_stem = f"{img_path.stem}_{ts}"
            try:
                img_path.rename(history_dir / f"{archive_stem}.png")
            except OSError:
                pass  # Best-effort archival; don't block the save
            if json_path.exists():
                try:
                    json_path.rename(history_dir / f"{archive_stem}.json")
                except OSError:
                    pass

        # Save image
        img_path.write_bytes(image)

        # Save metadata -- preserve original created_at (P7-F4)
        info = BaselineInfo(
            name=name,
            content_hash=content_hash,
            created_at=original_created_at,
            updated_at=now,
            branch=branch,
            viewport=viewport,
            approved_by=approved_by,
            metadata=metadata or {},
        )

        json_path.write_text(json.dumps(info.to_dict(), indent=2))

        logger.info(f"Saved baseline: {name} (branch={branch})")
        return info

    def delete_baseline(self, name: str, branch: str, viewport: Optional[str] = None) -> bool:
        """Delete a baseline."""
        img_path = self._get_path(name, branch, viewport, "png")
        json_path = self._get_path(name, branch, viewport, "json")

        deleted = False
        if img_path.exists():
            img_path.unlink()
            deleted = True
        if json_path.exists():
            json_path.unlink()
            deleted = True

        return deleted

    def list_baselines(self, branch: Optional[str] = None) -> List[BaselineInfo]:
        """List all baselines."""
        baselines = []

        if branch:
            branches = [branch]
        else:
            branches = [d.name for d in self.base_dir.iterdir() if d.is_dir()]

        for b in branches:
            branch_dir = self.base_dir / b.replace("/", "_")
            if not branch_dir.exists():
                continue

            for json_file in branch_dir.glob("*.json"):
                info = self.get_baseline_info(json_file.stem, b)
                if info:
                    baselines.append(info)

        return baselines


class SupabaseBaselineStorage(BaselineStorage):
    """Store baselines in Supabase storage.

    Good for CI/production where baselines need to be shared.
    """

    def __init__(
        self,
        project_id: str,
        supabase_url: Optional[str] = None,
        supabase_key: Optional[str] = None,
        bucket: str = "visual-baselines",
    ):
        """Initialize Supabase storage.

        Args:
            project_id: Hikaflow project ID
            supabase_url: Supabase URL (from env if not provided)
            supabase_key: Supabase service key (from env if not provided)
            bucket: Storage bucket name
        """
        self.project_id = project_id
        self.supabase_url = supabase_url or os.environ.get("SUPABASE_URL")
        self.supabase_key = supabase_key or os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
        self.bucket = bucket
        self._client = None

    def _get_client(self):
        """Get Supabase client."""
        if self._client is None:
            try:
                from supabase import create_client
                self._client = create_client(self.supabase_url, self.supabase_key)
            except ImportError:
                raise RuntimeError("supabase-py not installed. Run: pip install supabase")
        return self._client

    def _get_path(self, name: str, branch: str, viewport: Optional[str]) -> str:
        """Get storage path for baseline."""
        safe_name = name.replace("/", "_").replace(" ", "_")
        if viewport:
            safe_name = f"{safe_name}_{viewport}"
        return f"{self.project_id}/{branch.replace('/', '_')}/{safe_name}"

    def get_baseline(self, name: str, branch: str, viewport: Optional[str] = None) -> Optional[bytes]:
        """Get baseline from Supabase storage."""
        try:
            client = self._get_client()
            path = self._get_path(name, branch, viewport) + ".png"

            response = client.storage.from_(self.bucket).download(path)
            return response

        except Exception as e:
            if "not found" in str(e).lower():
                # Try main branch fallback
                if branch != "main":
                    return self.get_baseline(name, "main", viewport)
                return None
            logger.warning(f"Failed to get baseline from Supabase: {e}")
            return None

    def get_baseline_info(self, name: str, branch: str, viewport: Optional[str] = None) -> Optional[BaselineInfo]:
        """Get baseline info from Supabase."""
        try:
            client = self._get_client()
            path = self._get_path(name, branch, viewport) + ".json"

            response = client.storage.from_(self.bucket).download(path)
            data = json.loads(response.decode("utf-8"))

            return BaselineInfo(
                name=data["name"],
                content_hash=data["content_hash"],
                created_at=datetime.fromisoformat(data["created_at"]),
                updated_at=datetime.fromisoformat(data["updated_at"]),
                branch=data["branch"],
                viewport=data.get("viewport"),
                approved_by=data.get("approved_by"),
                metadata=data.get("metadata", {}),
            )

        except Exception as e:
            if "not found" in str(e).lower():
                if branch != "main":
                    return self.get_baseline_info(name, "main", viewport)
                return None
            logger.warning(f"Failed to get baseline info from Supabase: {e}")
            return None

    def save_baseline(
        self,
        name: str,
        image: bytes,
        branch: str,
        viewport: Optional[str] = None,
        approved_by: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> BaselineInfo:
        """Save baseline to Supabase storage."""
        client = self._get_client()
        now = datetime.now(timezone.utc)
        content_hash = hashlib.sha256(image).hexdigest()[:16]

        base_path = self._get_path(name, branch, viewport)

        # Upload image
        client.storage.from_(self.bucket).upload(
            base_path + ".png",
            image,
            {"content-type": "image/png", "upsert": "true"},
        )

        # Create info
        info = BaselineInfo(
            name=name,
            content_hash=content_hash,
            created_at=now,
            updated_at=now,
            branch=branch,
            viewport=viewport,
            approved_by=approved_by,
            metadata=metadata or {},
        )

        # Upload metadata
        client.storage.from_(self.bucket).upload(
            base_path + ".json",
            json.dumps(info.to_dict()).encode("utf-8"),
            {"content-type": "application/json", "upsert": "true"},
        )

        logger.info(f"Saved baseline to Supabase: {name} (branch={branch})")
        return info

    def delete_baseline(self, name: str, branch: str, viewport: Optional[str] = None) -> bool:
        """Delete baseline from Supabase."""
        try:
            client = self._get_client()
            base_path = self._get_path(name, branch, viewport)

            client.storage.from_(self.bucket).remove([
                base_path + ".png",
                base_path + ".json",
            ])
            return True

        except Exception as e:
            logger.warning(f"Failed to delete baseline from Supabase: {e}")
            return False

    def list_baselines(self, branch: Optional[str] = None) -> List[BaselineInfo]:
        """List baselines from Supabase."""
        baselines = []

        try:
            client = self._get_client()
            prefix = f"{self.project_id}/"
            if branch:
                prefix += f"{branch.replace('/', '_')}/"

            response = client.storage.from_(self.bucket).list(prefix)

            for item in response:
                if item["name"].endswith(".json"):
                    # Parse branch and name from path
                    parts = item["name"].replace(".json", "").split("/")
                    if len(parts) >= 2:
                        b = parts[-2]
                        n = parts[-1]
                        info = self.get_baseline_info(n, b)
                        if info:
                            baselines.append(info)

        except Exception as e:
            logger.warning(f"Failed to list baselines from Supabase: {e}")

        return baselines


def get_storage(
    storage_type: str = "auto",
    project_id: Optional[str] = None,
    base_dir: Optional[Path] = None,
) -> BaselineStorage:
    """Factory function to get appropriate storage backend.

    Args:
        storage_type: "local", "supabase", or "auto"
        project_id: Hikaflow project ID (for Supabase)
        base_dir: Base directory (for local)

    Returns:
        BaselineStorage instance
    """
    if storage_type == "local":
        return LocalBaselineStorage(base_dir)

    if storage_type == "supabase":
        if not project_id:
            raise ValueError("project_id required for Supabase storage")
        return SupabaseBaselineStorage(project_id)

    # Auto-detect
    if os.environ.get("SUPABASE_URL") and project_id:
        return SupabaseBaselineStorage(project_id)

    return LocalBaselineStorage(base_dir)
