import os
import time
import json
import re
import inspect
from dataclasses import dataclass
from typing import Optional, Union, List, Dict


@dataclass
class Cache:
    """
    Declarative configuration for page caching.
    
    Usage in index.py:
        from casp.cache_handler import Cache
        Cache(ttl=600, enabled=True)
    """
    ttl: int = 600
    enabled: bool = True

    def __post_init__(self):
        # Auto-register to module globals (Magic Registration)
        try:
            # Stack Depth:
            # 0. __post_init__ (this function)
            # 1. __init__ (generated by dataclass)
            # 2. The module calling Cache(...) (e.g. index.py)
            frame = inspect.currentframe()
            if frame and frame.f_back and frame.f_back.f_back:
                caller_frame = frame.f_back.f_back
                
                # Only auto-register if called at module level
                if caller_frame.f_code.co_name == '<module>':
                    caller_frame.f_globals['cache_settings'] = self
        except Exception:
            pass


class CacheHandler:
    CACHE_DIR = os.path.join(os.getcwd(), 'caches')
    MANIFEST_FILE = os.path.join(CACHE_DIR, 'cache_manifest.json')

    @classmethod
    def ensure_cache_dir(cls):
        """Ensure cache dir and manifest file exist."""
        if not os.path.exists(cls.CACHE_DIR):
            os.makedirs(cls.CACHE_DIR, exist_ok=True)

        if not os.path.exists(cls.MANIFEST_FILE):
            cls._write_manifest({})

    @classmethod
    def _read_manifest(cls) -> Dict:
        if not os.path.exists(cls.MANIFEST_FILE):
            return {}
        try:
            with open(cls.MANIFEST_FILE, 'r', encoding='utf-8') as f:
                return json.load(f)
        except (json.JSONDecodeError, OSError):
            return {}

    @classmethod
    def _write_manifest(cls, data: Dict):
        try:
            with open(cls.MANIFEST_FILE, 'w', encoding='utf-8') as f:
                json.dump(data, f, indent=2)
        except OSError as e:
            print(f"[Cache] Error writing manifest: {e}")

    @staticmethod
    def get_filename(uri: str) -> str:
        clean_uri = uri.split('?')[0]
        if clean_uri == '/' or clean_uri == '':
            filename = 'index'
        else:
            filename = re.sub(r'[^a-zA-Z0-9\-_]', '_', clean_uri).lower()
            filename = filename.strip('_')
        return f"{filename}.html"

    @classmethod
    def serve_cache(cls, uri: str, default_ttl: int = 600) -> Optional[str]:
        if not uri:
            return None

        cls.ensure_cache_dir()
        manifest = cls._read_manifest()

        if uri not in manifest:
            return None

        entry = manifest[uri]
        html_file = os.path.join(cls.CACHE_DIR, entry['file'])

        if not os.path.exists(html_file):
            cls.invalidate_by_uri(uri)
            return None

        saved_ttl = entry.get('ttl', default_ttl)
        created_at = entry.get('created_at', 0)
        active_ttl = saved_ttl if saved_ttl > 0 else default_ttl
        age = time.time() - created_at

        if age > active_ttl:
            cls.invalidate_by_uri(uri)
            return None

        try:
            with open(html_file, 'r', encoding='utf-8') as f:
                content = f.read()
            return content
        except Exception:
            return None

    @classmethod
    def save_cache(cls, uri: str, content: str, ttl: int = 0):
        if not uri:
            return

        cls.ensure_cache_dir()
        filename = cls.get_filename(uri)
        html_path = os.path.join(cls.CACHE_DIR, filename)

        manifest = cls._read_manifest()
        manifest[uri] = {
            "file": filename,
            "ttl": ttl,
            "created_at": time.time()
        }
        cls._write_manifest(manifest)

        try:
            with open(html_path, 'w', encoding='utf-8') as f:
                f.write(content)
            print(f"[Cache] Saved: {uri} (TTL: {ttl}s)")
        except Exception as e:
            print(f"[Cache] Error saving HTML: {e}")

    @classmethod
    def invalidate_by_uri(cls, uris: Union[str, List[str]]):
        if isinstance(uris, str):
            uris = [uris]

        cls.ensure_cache_dir()
        manifest = cls._read_manifest()
        updated = False

        for uri in uris:
            normalized_uri = '/' + uri.lstrip('/')
            if normalized_uri in manifest:
                entry = manifest[normalized_uri]
                file_path = os.path.join(cls.CACHE_DIR, entry['file'])
                if os.path.exists(file_path):
                    try:
                        os.remove(file_path)
                    except OSError:
                        pass
                del manifest[normalized_uri]
                updated = True
                print(f"[Cache] Invalidated: {normalized_uri}")

        if updated:
            cls._write_manifest(manifest)

    @classmethod
    def reset_cache(cls, uri: Optional[str] = None):
        if uri:
            cls.invalidate_by_uri(uri)
            return
        if os.path.exists(cls.CACHE_DIR):
            for f in os.listdir(cls.CACHE_DIR):
                file_path = os.path.join(cls.CACHE_DIR, f)
                try:
                    os.remove(file_path)
                except OSError:
                    pass
        cls.ensure_cache_dir()
        cls._write_manifest({})