import json
import mimetypes
import os
import re
from re import Pattern

from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import Response
from starlette.staticfiles import StaticFiles
from starlette.types import Receive, Scope, Send

from .constants import HASHED_FILE_REGEX, ROOT_PATH_REGEX, ROUTE_PARAM_REGEX


class SPAStaticFiles(StaticFiles):
    def __init__(self, directory: str, api_prefix: str = "api/"):
        self.api_prefix = api_prefix.strip("/")
        self.build_dir = directory

        # Split routes into static (O(1) lookup) and dynamic (Regex)
        self.static_routes: set[str] = set()
        self.dynamic_routes: list[Pattern] = []

        # Preloaded responses
        self.index_response: Response | None = None
        self.not_found_response: Response | None = None
        self.file_cache: dict[str, Response] = {}

        self.load_manifest()
        self.preload_all_assets()
        super().__init__(directory=directory, html=True)

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        await super().__call__(scope, receive, send)

    def load_manifest(self):
        manifest_path = os.path.join(self.build_dir, "spa-manifest.json")
        if os.path.exists(manifest_path):
            try:
                with open(manifest_path, encoding="utf-8") as f:
                    data = json.load(f)
                    routes = data.get("routes", [])

                    for route in routes:
                        if self._is_static_route(route):
                            # Normalize: ensure leading slash, no trailing slash (unless root)
                            normalized = "/" + route.strip("/")
                            if normalized == "//":
                                normalized = "/"
                            self.static_routes.add(normalized)
                        else:
                            self.dynamic_routes.append(self._route_to_regex(route))

                    print(
                        f"Loaded {len(self.static_routes)} static routes and {len(self.dynamic_routes)} dynamic routes from manifest."
                    )
            except Exception as e:
                print(f"Error loading SPA manifest: {e}")
        else:
            print(
                f"SPA manifest not found at {manifest_path}. SPA fallback validation disabled."
            )

    def preload_all_assets(self):
        """Preload all static files into memory for extreme performance."""
        print(f"Preloading all assets from {self.build_dir}...")
        count = 0
        total_size = 0

        for root, _, files in os.walk(self.build_dir):
            for filename in files:
                file_path = os.path.join(root, filename)
                rel_path = os.path.relpath(file_path, self.build_dir)
                # Normalize to URL path (forward slashes)
                url_path = rel_path.replace("\\", "/")

                try:
                    with open(file_path, "rb") as f:
                        content = f.read()
                        media_type, _ = mimetypes.guess_type(file_path)
                        response = Response(
                            content=content,
                            media_type=media_type or "application/octet-stream",
                        )

                        # Apply aggressive caching for hashed files (Vite default: name-hash.ext)
                        # Hash is typically 8 chars of alphanumeric + '-' + '_'
                        if HASHED_FILE_REGEX.search(filename):
                            response.headers["Cache-Control"] = (
                                "public, max-age=31536000, immutable"
                            )

                        self.file_cache[url_path] = response

                        # Set special responses
                        if url_path == "index.html":
                            self.index_response = Response(
                                content=content, media_type="text/html"
                            )
                        elif url_path == "404.html":
                            self.not_found_response = Response(
                                content=content, status_code=404, media_type="text/html"
                            )

                        count += 1
                        total_size += len(content)
                except Exception as e:
                    print(f"Error caching {file_path}: {e}")

        print(f"Cached {count} files ({total_size / 1024 / 1024:.2f} MB) in memory.")

    def _is_static_route(self, route: str) -> bool:
        """Check if a route contains any dynamic parameters."""
        return ":" not in route and "*" not in route and "(" not in route

    def _route_to_regex(self, route: str) -> Pattern:
        """
        Convert Vue Router path to Regex Pattern.
        """
        if route == "/":
            return ROOT_PATH_REGEX

        parts = route.split("/")
        pattern = "^"

        for part in parts:
            if not part:
                continue

            # Regex to identify params:
            # Group 1: :name
            # Group 2: (custom_regex) - optional
            # Group 3: ? - optional

            segment_pattern = ""
            last_idx = 0
            is_optional_segment = False

            matches = list(ROUTE_PARAM_REGEX.finditer(part))

            if not matches:
                # Static segment
                segment_pattern = re.escape(part)
            else:
                for match in matches:
                    # Append static text before match
                    static_text = part[last_idx : match.start()]
                    segment_pattern += re.escape(static_text)

                    # Handle Param
                    custom_regex = match.group(2)
                    is_optional = bool(match.group(3))

                    if custom_regex:
                        # Strip parens to get the regex content: :slug(.*) -> (.*) -> .*
                        inner_regex = custom_regex[1:-1]
                        segment_pattern += inner_regex
                    else:
                        segment_pattern += r"[^/]+"

                    # Logic for optional segment:
                    # If the match covers the ENTIRE part and is optional, the whole segment (including slash) is optional
                    if is_optional and len(part) == len(match.group(0)):
                        is_optional_segment = True

                    last_idx = match.end()

                # Append remaining static text
                segment_pattern += re.escape(part[last_idx:])

            # Append to main pattern
            if is_optional_segment:
                pattern += f"(?:/{segment_pattern})?"
            else:
                pattern += f"/{segment_pattern}"

        pattern += "$"
        return re.compile(pattern)

    def is_valid_route(self, path: str) -> bool:
        # If no routes loaded (no manifest), allow all (backward compatibility)
        if not self.static_routes and not self.dynamic_routes:
            return True

        # Ensure path starts with /
        check_path = path if path.startswith("/") else "/" + path

        # 1. Fast O(1) Lookup
        if check_path in self.static_routes:
            return True

        # 2. Regex Lookup
        for pattern in self.dynamic_routes:
            if pattern.match(check_path):
                return True
        return False

    async def get_response(self, path: str, scope: Scope):
        # Normalize path separators for Windows
        lookup_path = path.replace("\\", "/")

        # 0. Check memory cache
        if lookup_path in self.file_cache:
            return self.file_cache[lookup_path]

        # Check for index.html in directory (simulating StaticFiles behavior)
        potential_index = f"{lookup_path}/index.html".lstrip("/")
        if potential_index in self.file_cache:
            return self.file_cache[potential_index]

        try:
            # 1. Try to serve static file directly
            response = await super().get_response(path, scope)
            if response.status_code == 404:
                raise StarletteHTTPException(status_code=404)
            return response
        except (StarletteHTTPException, OSError) as e:
            # Handle 404s and OSErrors (e.g., invalid filenames on Windows like 'test:case')
            is_404 = isinstance(e, StarletteHTTPException) and e.status_code == 404
            is_os_error = isinstance(e, OSError)

            if not (is_404 or is_os_error):
                raise e

            # 2. If not found or invalid file path, check if it is an API route
            # API routes should strictly fail if not handled by FastAPI router
            if path.startswith(self.api_prefix) or path.startswith(
                f"{self.api_prefix}/"
            ):
                raise e

            # 3. Validate against SPA routes (Strict Mode)
            # Normalize path separators for Windows and construct request path
            normalized_path = path.replace("\\", "/")
            request_path = "/" + normalized_path.lstrip("/")

            if self.is_valid_route(request_path):
                if self.index_response:
                    return self.index_response

            # 4. If not a valid route, return 404.html if exists, else propagate exception
            if self.not_found_response:
                return self.not_found_response

            raise e
