from __future__ import annotations

import json
import logging
import re
import sqlite3
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, quote, unquote, urlparse


class RedisCache:
    def __init__(self, redis_url: str | None, ttl: int):
        self.ttl = ttl
        self.client = None
        if not redis_url:
            return
        try:
            import redis

            self.client = redis.Redis.from_url(redis_url, decode_responses=True)
            self.client.ping()
        except Exception as exc:
            logging.warning("Redis cache disabled: %s", exc)
            self.client = None

    def get_json(self, key: str) -> Any | None:
        if not self.client:
            return None
        value = self.client.get(key)
        return json.loads(value) if value else None

    def set_json(self, key: str, value: Any) -> None:
        if self.client:
            self.client.setex(key, self.ttl, json.dumps(value, ensure_ascii=False))

    def get_text(self, key: str) -> str | None:
        if not self.client:
            return None
        return self.client.get(key)

    def set_text(self, key: str, value: str) -> None:
        if self.client:
            self.client.setex(key, self.ttl, value)


class NuvioRepository:
    def __init__(self, db_path: str, content_root: str, playlist_mount: str):
        self.db_uri = Path(db_path).resolve().as_uri() + "?mode=ro"
        self.content_root = Path(content_root).resolve()
        self.playlist_mount = playlist_mount.strip("/")

    def streams(
        self,
        media_type: str,
        id_kind: str,
        item_id: str,
        season: int | None,
        episode: int | None,
        playlist_public_url: str,
    ) -> list[dict[str, Any]]:
        id_column = "m.id" if id_kind == "webmovies" else "m.imdb_id" if id_kind == "imdb" else "m.tmdb_id"
        query = """
            SELECT e.access_key, e.filename, e.season, e.episode, e.cleaned_m3u8_path,
                   e.crawl_status, s.name AS server_name, e.source, m.title, m.year
            FROM episodes e
            JOIN movies m ON m.id = e.movie_id
            LEFT JOIN servers s ON s.id = e.server_id
            WHERE m.media_type = ? AND {id_column} = ? AND e.crawl_status = 'success'
        """
        query = query.format(id_column=id_column)
        params: list[Any] = [media_type, item_id]
        if media_type == "tv" and season is not None and episode is not None:
            query += " AND e.season = ? AND e.episode = ?"
            params.extend([season, episode])
        query += " ORDER BY e.source, s.priority, e.id"

        with sqlite3.connect(self.db_uri, uri=True) as conn:
            conn.row_factory = sqlite3.Row
            rows = conn.execute(query, params).fetchall()

        streams = []
        for row in rows:
            source = str(row["source"] or "").upper()
            server = row["server_name"] or row["filename"] or "Server"
            title = f"{source} - {server}"
            if media_type == "tv":
                title = f"S{int(row['season']):02}E{int(row['episode']):02} - {title}"
            streams.append(
                {
                    "name": "WebMovies",
                    "title": title,
                    "url": f"{playlist_public_url.rstrip('/')}/{quote(row['access_key'])}.m3u8",
                }
            )
        return streams

    def catalog(self, media_type: str, search: str | None = None, limit: int = 100) -> list[dict[str, Any]]:
        query = """
            SELECT m.id, m.tmdb_id, m.imdb_id, m.media_type, m.title, m.original_title, m.year,
                   m.poster_url, m.backdrop_url, m.content, MAX(m.updated_at) AS updated_at
            FROM movies m
            WHERE m.media_type = ?
              AND EXISTS (
                  SELECT 1 FROM episodes e
                  WHERE e.movie_id = m.id AND e.crawl_status = 'success'
              )
        """
        params: list[Any] = [media_type]
        if search:
            query += " AND (m.title LIKE ? OR m.original_title LIKE ? OR m.tmdb_id = ? OR m.imdb_id = ? OR CAST(m.id AS TEXT) = ?)"
            like = f"%{search}%"
            params.extend([like, like, search, search, search.removeprefix("webmovies:")])
        query += " GROUP BY m.id ORDER BY m.updated_at DESC LIMIT ?"
        params.append(limit)

        with sqlite3.connect(self.db_uri, uri=True) as conn:
            conn.row_factory = sqlite3.Row
            rows = conn.execute(query, params).fetchall()

        metas = []
        for row in rows:
            item_type = "movie" if row["media_type"] == "movie" else "series"
            meta_id = external_or_local_id(row)
            meta = {
                "id": meta_id,
                "type": item_type,
                "name": row["title"],
                "poster": row["poster_url"],
                "background": row["backdrop_url"],
                "description": row["content"],
            }
            if row["year"]:
                meta["releaseInfo"] = str(row["year"])
            metas.append(meta)
        return metas

    def meta(self, media_type: str, id_kind: str, item_id: str) -> dict[str, Any] | None:
        id_column = "m.id" if id_kind == "webmovies" else "m.imdb_id" if id_kind == "imdb" else "m.tmdb_id"
        query = f"""
            SELECT m.id, m.tmdb_id, m.imdb_id, m.media_type, m.title, m.original_title, m.year,
                   m.poster_url, m.backdrop_url, m.content
            FROM movies m
            WHERE m.media_type = ? AND {id_column} = ?
            LIMIT 1
        """
        with sqlite3.connect(self.db_uri, uri=True) as conn:
            conn.row_factory = sqlite3.Row
            movie = conn.execute(query, (media_type, item_id)).fetchone()
            if not movie:
                return None
            videos = []
            if media_type == "tv":
                video_rows = conn.execute(
                    """
                    SELECT season, episode, title
                    FROM episodes
                    WHERE movie_id = ? AND crawl_status = 'success'
                    GROUP BY season, episode
                    ORDER BY season, episode
                    """,
                    (movie["id"],),
                ).fetchall()
                base_id = external_or_local_id(movie)
                for row in video_rows:
                    season = int(row["season"])
                    episode = int(row["episode"])
                    videos.append(
                        {
                            "id": f"{base_id}:{season}:{episode}",
                            "title": row["title"] or f"S{season:02}E{episode:02}",
                            "season": season,
                            "episode": episode,
                        }
                    )

        item_type = "movie" if movie["media_type"] == "movie" else "series"
        meta_id = external_or_local_id(movie)
        meta = {
            "id": meta_id,
            "type": item_type,
            "name": movie["title"],
            "poster": movie["poster_url"],
            "background": movie["backdrop_url"],
            "description": movie["content"],
        }
        if movie["year"]:
            meta["releaseInfo"] = str(movie["year"])
        if videos:
            meta["videos"] = videos
        return meta

    def playlist_content(self, access_key: str) -> str | None:
        with sqlite3.connect(self.db_uri, uri=True) as conn:
            row = conn.execute(
                "SELECT cleaned_m3u8_path FROM episodes WHERE access_key = ? AND crawl_status = 'success'",
                (access_key,),
            ).fetchone()
        if not row or not row[0]:
            return None
        path = self._db_playlist_path_to_file(row[0])
        if not path.exists() or not path.is_file():
            return None
        return path.read_text(encoding="utf-8")

    def _db_playlist_path_to_file(self, db_path: str) -> Path:
        prefix = f"/{self.playlist_mount}/"
        if db_path.startswith(prefix):
            return self.content_root / db_path.lstrip("/")
        return self.content_root / self.playlist_mount / db_path.lstrip("/")


def parse_stream_request(path: str) -> tuple[str, str, str, int | None, int | None] | None:
    match = re.match(r"^/stream/([^/]+)/(.+)\.json$", path)
    if not match:
        return None
    raw_type = match.group(1).lower()
    media_type = "movie" if raw_type in {"movie", "movies"} else "tv"
    item_id = unquote(match.group(2)).replace("/", ":")
    parts = item_id.split(":")
    if parts[0] == "tmdb" and len(parts) >= 2:
        tmdb_id = parts[1]
        season = int(parts[2]) if len(parts) >= 4 and parts[2].isdigit() else None
        episode = int(parts[3]) if len(parts) >= 4 and parts[3].isdigit() else None
        return media_type, "tmdb", tmdb_id, season, episode
    if parts[0] == "webmovies" and len(parts) >= 2:
        movie_id = parts[1]
        season = int(parts[2]) if len(parts) >= 4 and parts[2].isdigit() else None
        episode = int(parts[3]) if len(parts) >= 4 and parts[3].isdigit() else None
        return media_type, "webmovies", movie_id, season, episode
    if item_id.startswith("tt"):
        season = int(parts[1]) if len(parts) >= 3 and parts[1].isdigit() else None
        episode = int(parts[2]) if len(parts) >= 3 and parts[2].isdigit() else None
        return media_type, "imdb", parts[0], season, episode
    if item_id.isdigit():
        return media_type, "tmdb", item_id, None, None
    return None


def parse_meta_request(path: str) -> tuple[str, str, str] | None:
    match = re.match(r"^/meta/([^/]+)/(.+)\.json$", path)
    if not match:
        return None
    raw_type = match.group(1).lower()
    media_type = "movie" if raw_type in {"movie", "movies"} else "tv"
    item_id = unquote(match.group(2)).replace("/", ":")
    parts = item_id.split(":")
    if parts[0] == "tmdb" and len(parts) >= 2:
        return media_type, "tmdb", parts[1]
    if parts[0] == "webmovies" and len(parts) >= 2:
        return media_type, "webmovies", parts[1]
    if item_id.startswith("tt"):
        return media_type, "imdb", parts[0]
    if item_id.isdigit():
        return media_type, "tmdb", item_id
    return None


def external_or_local_id(row: sqlite3.Row) -> str:
    if row["imdb_id"]:
        return row["imdb_id"]
    if row["tmdb_id"]:
        return f"tmdb:{row['tmdb_id']}"
    return f"webmovies:{row['id']}"


def run_nuvio_addon(config: dict) -> None:
    host = config.get("host", "0.0.0.0")
    port = int(config.get("port", 7000))
    addon_public_url = config.get("public_url", f"http://localhost:{port}")
    playlist_public_url = config.get("playlist_public_url") or addon_public_url
    cache = RedisCache(config.get("redis_url"), int(config.get("cache_ttl", 300)))
    repo = NuvioRepository(config["db_path"], config.get("content_root", "."), config.get("playlist_mount", "/m3u8"))

    class Handler(BaseHTTPRequestHandler):
        def do_GET(self):
            parsed = urlparse(self.path)
            path = unquote(parsed.path)
            if path == "/health":
                return self._send_json({"status": "ok"})
            if path == "/manifest.json":
                return self._send_json(manifest(addon_public_url))
            if path.startswith("/meta/"):
                return self._handle_meta(path)
            if path.startswith("/stream/"):
                return self._handle_stream(path)
            if path.startswith("/catalog/"):
                return self._handle_catalog(path, parsed.query)
            playlist_match = re.match(r"^/(?:playlist/)?([^/]+)\.m3u8$", path)
            if playlist_match:
                return self._handle_playlist(playlist_match.group(1))
            self.send_error(404, "Not found")

        def _handle_stream(self, path: str):
            request = parse_stream_request(path)
            if not request:
                return self._send_json({"streams": []})
            media_type, id_kind, item_id, season, episode = request
            cache_key = f"nuvio:stream:{media_type}:{id_kind}:{item_id}:{season or 0}:{episode or 0}"
            cached = cache.get_json(cache_key)
            if cached is not None:
                return self._send_json(cached)
            payload = {"streams": repo.streams(media_type, id_kind, item_id, season, episode, playlist_public_url)}
            cache.set_json(cache_key, payload)
            return self._send_json(payload)

        def _handle_meta(self, path: str):
            request = parse_meta_request(path)
            if not request:
                return self._send_json({"meta": None})
            media_type, id_kind, item_id = request
            cache_key = f"nuvio:meta:{media_type}:{id_kind}:{item_id}"
            cached = cache.get_json(cache_key)
            if cached is not None:
                return self._send_json(cached)
            payload = {"meta": repo.meta(media_type, id_kind, item_id)}
            cache.set_json(cache_key, payload)
            return self._send_json(payload)

        def _handle_catalog(self, path: str, query: str):
            request = parse_catalog_request(path, query)
            if not request:
                return self._send_json({"metas": []})
            media_type, search = request
            cache_key = f"nuvio:catalog:{media_type}:{search or ''}"
            cached = cache.get_json(cache_key)
            if cached is not None:
                return self._send_json(cached)
            payload = {"metas": repo.catalog(media_type, search)}
            cache.set_json(cache_key, payload)
            return self._send_json(payload)

        def _handle_playlist(self, access_key: str):
            cache_key = f"nuvio:playlist:{access_key}"
            content = cache.get_text(cache_key)
            if content is None:
                content = repo.playlist_content(access_key)
                if content is None:
                    self.send_error(404, "Playlist not found")
                    return
                cache.set_text(cache_key, content)
            self.send_response(200)
            self.send_header("Content-Type", "application/vnd.apple.mpegurl; charset=utf-8")
            self.send_header("Cache-Control", "public, max-age=60")
            self.end_headers()
            self.wfile.write(content.encode("utf-8"))

        def _send_json(self, payload: dict[str, Any]):
            body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)

    logging.info("Serving Nuvio addon at http://%s:%s", host, port)
    ThreadingHTTPServer((host, port), Handler).serve_forever()


def parse_catalog_request(path: str, query: str) -> tuple[str, str | None] | None:
    match = re.match(r"^/catalog/([^/]+)/webmovies(?:/(.+))?\.json$", path)
    if not match:
        return None
    raw_type = match.group(1).lower()
    media_type = "movie" if raw_type in {"movie", "movies"} else "tv"
    extra = match.group(2) or ""
    search = None
    if extra.startswith("search="):
        search = extra.split("=", 1)[1]
    if search is None:
        params = parse_qs(query)
        search_values = params.get("search") or params.get("query")
        search = search_values[0] if search_values else None
    return media_type, unquote(search) if search else None


def manifest(public_url: str) -> dict[str, Any]:
    return {
        "id": "net.dongnq.webmovies",
        "version": "1.0.0",
        "name": "WebMovies",
        "description": "Streams from the WebMovies crawler database",
        "resources": ["stream", "catalog", "meta"],
        "types": ["movie", "series"],
        "catalogs": [
            {"type": "movie", "id": "webmovies", "name": "WebMovies Movies", "extra": [{"name": "search", "isRequired": True}]},
            {"type": "series", "id": "webmovies", "name": "WebMovies TVShows", "extra": [{"name": "search", "isRequired": True}]},
        ],
        "idPrefixes": ["tmdb:", "tt", "webmovies:"],
        "behaviorHints": {"configurable": False, "configurationRequired": False},
    }
