# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Page Archive for Private Browser.

Encrypted local storage for saved web pages with
full-text search capability.
"""

import json
import logging
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

# Optional encryption
try:
    from cryptography.fernet import Fernet

    HAS_CRYPTO = True
except ImportError:
    HAS_CRYPTO = False


class PageArchive:
    """
    Encrypted archive for saved web pages.

    Features:
    - Encrypted storage at rest
    - Full-text search
    - Metadata indexing
    - Automatic cleanup of old pages
    """

    def __init__(self, archive_path: Path, max_size_mb: int = 1000):
        self.archive_path = archive_path
        self.max_size_mb = max_size_mb

        # Encryption
        self._fernet = None
        self._key_file = archive_path / ".key"

        # Index (in-memory for fast search)
        self._index: Dict[str, Dict[str, Any]] = {}
        self._index_file = archive_path / "index.json"

    async def initialize(self):
        """Initialize archive."""
        self.archive_path.mkdir(parents=True, exist_ok=True)

        # Initialize encryption
        if HAS_CRYPTO:
            await self._init_encryption()

        # Load index
        await self._load_index()

        logger.info(f"Page archive initialized at {self.archive_path}")

    async def _init_encryption(self):
        """Initialize encryption key."""
        if self._key_file.exists():
            key = self._key_file.read_bytes()
        else:
            key = Fernet.generate_key()
            self._key_file.write_bytes(key)
            self._key_file.chmod(0o600)

        self._fernet = Fernet(key)

    async def _load_index(self):
        """Load search index from disk."""
        if self._index_file.exists():
            try:
                data = self._index_file.read_text()
                if self._fernet:
                    data = self._fernet.decrypt(data.encode()).decode()
                self._index = json.loads(data)
            except Exception as e:
                logger.error(f"Failed to load index: {e}")
                self._index = {}

    async def _save_index(self):
        """Save search index to disk."""
        data = json.dumps(self._index)
        if self._fernet:
            data = self._fernet.encrypt(data.encode()).decode()

        # Atomic write
        temp = self._index_file.with_suffix(".tmp")
        temp.write_text(data)
        temp.replace(self._index_file)

    async def save(self, result) -> str:
        """
        Save a browse result to the archive.

        Args:
            result: BrowseResult object

        Returns:
            Archive ID
        """
        # Generate ID
        archive_id = str(uuid.uuid4())

        # Prepare data
        data = {
            "id": archive_id,
            "url": result.url,
            "final_url": result.final_url,
            "title": result.title,
            "content_html": result.content_html,
            "content_text": result.content_text,
            "reader_html": result.reader_html,
            "summary": result.summary,
            "meta": result.meta,
            "timestamp": result.timestamp.isoformat(),
        }

        # Serialize
        content = json.dumps(data)

        # Encrypt
        if self._fernet:
            content = self._fernet.encrypt(content.encode()).decode()

        # Save to file
        page_file = self.archive_path / f"{archive_id}.page"
        page_file.write_text(content)

        # Update index
        self._index[archive_id] = {
            "url": result.url,
            "title": result.title,
            "timestamp": result.timestamp.isoformat(),
            "text_preview": result.content_text[:500] if result.content_text else "",
        }
        await self._save_index()

        # Check size limits
        await self._cleanup_if_needed()

        logger.info(f"Archived page: {result.title} ({archive_id})")
        return archive_id

    async def load(self, archive_id: str):
        """
        Load an archived page.

        Args:
            archive_id: Archive ID

        Returns:
            BrowseResult-like dict or None
        """
        page_file = self.archive_path / f"{archive_id}.page"

        if not page_file.exists():
            return None

        try:
            content = page_file.read_text()

            if self._fernet:
                content = self._fernet.decrypt(content.encode()).decode()

            data = json.loads(content)
            data["timestamp"] = datetime.fromisoformat(data["timestamp"])

            return data

        except Exception as e:
            logger.error(f"Failed to load archive {archive_id}: {e}")
            return None

    async def delete(self, archive_id: str) -> bool:
        """
        Delete an archived page.

        Args:
            archive_id: Archive ID

        Returns:
            True if deleted
        """
        page_file = self.archive_path / f"{archive_id}.page"

        if page_file.exists():
            page_file.unlink()

        if archive_id in self._index:
            del self._index[archive_id]
            await self._save_index()
            return True

        return False

    async def list(
        self,
        limit: int = 50,
        search: Optional[str] = None,
    ) -> List[Dict[str, Any]]:
        """
        List archived pages.

        Args:
            limit: Maximum results
            search: Optional search filter

        Returns:
            List of archive metadata
        """
        results = []

        for archive_id, meta in self._index.items():
            # Search filter
            if search:
                search_lower = search.lower()
                if (
                    search_lower not in meta.get("title", "").lower()
                    and search_lower not in meta.get("url", "").lower()
                    and search_lower not in meta.get("text_preview", "").lower()
                ):
                    continue

            results.append(
                {
                    "id": archive_id,
                    **meta,
                }
            )

        # Sort by timestamp (newest first)
        results.sort(key=lambda x: x.get("timestamp", ""), reverse=True)

        return results[:limit]

    async def search(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:
        """
        Full-text search across archived pages.

        Args:
            query: Search query
            limit: Maximum results

        Returns:
            List of matching archives with relevance
        """
        query_lower = query.lower()
        query_terms = query_lower.split()
        results = []

        for archive_id, meta in self._index.items():
            # Simple relevance scoring
            score = 0
            searchable = f"{meta.get('title', '')} {meta.get('url', '')} {meta.get('text_preview', '')}".lower()

            # Exact phrase match
            if query_lower in searchable:
                score += 10

            # Term matches
            for term in query_terms:
                if term in searchable:
                    score += 1
                    # Bonus for title match
                    if term in meta.get("title", "").lower():
                        score += 2

            if score > 0:
                results.append(
                    {
                        "id": archive_id,
                        "score": score,
                        **meta,
                    }
                )

        # Sort by score
        results.sort(key=lambda x: x["score"], reverse=True)

        return results[:limit]

    async def get_stats(self) -> Dict[str, Any]:
        """Get archive statistics."""
        total_size = 0
        page_count = 0

        for f in self.archive_path.glob("*.page"):
            total_size += f.stat().st_size
            page_count += 1

        return {
            "page_count": page_count,
            "total_size_bytes": total_size,
            "total_size_mb": round(total_size / (1024 * 1024), 2),
            "max_size_mb": self.max_size_mb,
            "encrypted": self._fernet is not None,
        }

    async def _cleanup_if_needed(self):
        """Remove old pages if archive exceeds size limit."""
        stats = await self.get_stats()

        if stats["total_size_mb"] <= self.max_size_mb:
            return

        # Sort by timestamp (oldest first)
        pages = sorted(
            self._index.items(),
            key=lambda x: x[1].get("timestamp", ""),
        )

        # Remove oldest until under limit
        removed = 0
        target_size = self.max_size_mb * 0.9 * 1024 * 1024  # 90% of limit

        for archive_id, meta in pages:
            if stats["total_size_bytes"] <= target_size:
                break

            page_file = self.archive_path / f"{archive_id}.page"
            if page_file.exists():
                size = page_file.stat().st_size
                page_file.unlink()
                stats["total_size_bytes"] -= size
                del self._index[archive_id]
                removed += 1

        if removed > 0:
            await self._save_index()
            logger.info(f"Cleaned up {removed} old archived pages")

    async def export(self, archive_id: str, format: str = "html") -> Optional[str]:
        """
        Export an archived page.

        Args:
            archive_id: Archive ID
            format: Export format (html, text, markdown)

        Returns:
            Exported content
        """
        data = await self.load(archive_id)
        if not data:
            return None

        if format == "html":
            return data.get("reader_html") or data.get("content_html", "")

        elif format == "text":
            return data.get("content_text", "")

        elif format == "markdown":
            # Simple HTML to Markdown conversion
            html = data.get("content_html", "")
            text = html

            # Headers
            text = re.sub(r"<h1[^>]*>([^<]+)</h1>", r"# \1\n", text, flags=re.IGNORECASE)
            text = re.sub(r"<h2[^>]*>([^<]+)</h2>", r"## \1\n", text, flags=re.IGNORECASE)
            text = re.sub(r"<h3[^>]*>([^<]+)</h3>", r"### \1\n", text, flags=re.IGNORECASE)

            # Links
            text = re.sub(
                r'<a[^>]+href="([^"]+)"[^>]*>([^<]+)</a>', r"[\2](\1)", text, flags=re.IGNORECASE
            )

            # Bold/italic
            text = re.sub(r"<strong>([^<]+)</strong>", r"**\1**", text, flags=re.IGNORECASE)
            text = re.sub(r"<em>([^<]+)</em>", r"*\1*", text, flags=re.IGNORECASE)

            # Lists
            text = re.sub(r"<li>([^<]+)</li>", r"- \1\n", text, flags=re.IGNORECASE)

            # Remove remaining tags
            text = re.sub(r"<[^>]+>", "", text)

            # Clean whitespace
            text = re.sub(r"\n{3,}", "\n\n", text)

            return f"# {data.get('title', 'Untitled')}\n\n{text.strip()}"

        return None


# Import re for markdown export
import re  # noqa: E402
