from __future__ import annotations

import json
import os
import sqlite3
import uuid
import hashlib
from datetime import datetime
from pathlib import Path
from typing import Any

EMBY_USERS_CACHE_PATH = Path(os.environ.get("EMBY_USER_CACHE_PATH", "cache/homescreen-emby-users.json"))
EMBY_USER_CONTEXT_CACHE_PATH = Path(os.environ.get("EMBY_USER_CONTEXT_CACHE_PATH", "cache/homescreen-emby-user-context.json"))
HOMESCREEN_UPLOAD_DIR = Path(os.environ.get("HOMESCREEN_UPLOAD_DIR", "cache/homescreen_uploads"))
HOMESCREEN_UPLOAD_STATE_PATH = Path(os.environ.get("HOMESCREEN_UPLOAD_STATE_PATH", "cache/homescreen-upload-state.json"))


SECTION_TYPES = [
    {"value": "resume", "label": "Resume / Next Up"},
    {"value": "items", "label": "Items (filtered)"},
    {"value": "userviews", "label": "Libraries"},
    {"value": "boxset", "label": "Box Set"},
    {"value": "collections", "label": "Collections"},
    {"value": "latestepisodereleases", "label": "Latest episode releases"},
    {"value": "latestmoviereleases", "label": "Latest movie releases"},
    {"value": "latestmediablock", "label": "Latest media"},
]

COLLECTION_TYPES = [
    {"value": "", "label": "(none)"},
    {"value": "movies", "label": "Movies"},
    {"value": "tvshows", "label": "TV Shows"},
    {"value": "boxsets", "label": "Box Sets"},
]

ITEM_TYPES = ["Movie", "Series", "Episode", "BoxSet"]

SORT_OPTIONS = [
    {"value": "", "label": "(none)"},
    {"value": "default", "label": "Default (boxset)"},
    {"value": "DatePlayed", "label": "Date played"},
    {"value": "DateLastContentAdded,SortName", "label": "Date added"},
    {"value": "ProductionYear,PremiereDate,SortName", "label": "Release year"},
    {"value": "CommunityRating", "label": "Community rating"},
    {"value": "CriticRating,SortName", "label": "Critic rating"},
    {"value": "DateCreated,SortName", "label": "Date created"},
    {"value": "Random", "label": "Random"},
    {"value": "SortName", "label": "Name"},
]

IMAGE_TYPES = [
    {"value": "", "label": "Default"},
    {"value": "Thumb", "label": "Thumb"},
    {"value": "Primary", "label": "Primary / Poster"},
]


def enums_payload() -> dict[str, Any]:
    return {
        "section_types": SECTION_TYPES,
        "collection_types": COLLECTION_TYPES,
        "item_types": ITEM_TYPES,
        "sort_options": SORT_OPTIONS,
        "image_types": IMAGE_TYPES,
    }


def normalize_guid(value: str | None) -> str:
    return str(value or "").replace("-", "").strip().lower()


def gen_id() -> str:
    return uuid.uuid4().hex[:32]


def create_empty_section(user_id: str) -> dict[str, Any]:
    return {
        "UserId": user_id,
        "Name": "New Section",
        "CustomName": "New Section",
        "Id": gen_id(),
        "SectionType": "items",
        "ImageType": "Thumb",
        "CollectionType": "movies",
        "SortBy": "Random",
        "SortOrder": "Descending",
        "Monitor": [],
        "ItemTypes": ["Movie"],
        "ExcludedFolders": [],
        "CardSizeOffset": 0,
        "IncludeNextUpInResume": True,
        "Query": {
            "StudioIds": [],
            "TagIds": [],
            "GenreIds": [],
            "CollectionTypes": [],
            "IsPlayed": False,
        },
    }


def create_recently_watched_section(user_id: str, user_name: str = "") -> dict[str, Any]:
    label = f"Recently Watched - {user_name}" if user_name else "Recently Watched"
    return {
        "UserId": user_id,
        "Name": label,
        "CustomName": label,
        "Id": gen_id(),
        "SectionType": "items",
        "ImageType": "Thumb",
        "CollectionType": "",
        "SortBy": "DatePlayed",
        "SortOrder": "Descending",
        "Monitor": [],
        "ItemTypes": ["Movie", "Series"],
        "ExcludedFolders": [],
        "CardSizeOffset": 0,
        "IncludeNextUpInResume": True,
        "Query": {
            "StudioIds": [],
            "TagIds": [],
            "GenreIds": [],
            "CollectionTypes": [],
            "IsPlayed": True,
        },
    }


def create_boxset_section(user_id: str, collection_name: str = "", collection_id: str = "") -> dict[str, Any]:
    label = collection_name or "New Collection"
    return {
        "UserId": user_id,
        "Name": label,
        "CustomName": label,
        "Id": gen_id(),
        "SectionType": "boxset",
        "ImageType": "Thumb",
        "ItemTypes": [],
        "SortBy": "Random",
        "SortOrder": "Descending",
        "Monitor": [],
        "ExcludedFolders": [],
        "CardSizeOffset": 0,
        "IncludeNextUpInResume": True,
        "ParentItem": {
            "Name": label,
            "Id": str(collection_id or ""),
        },
        "ParentId": str(collection_id or ""),
    }


def normalize_sections_for_user(sections: Any, expected_emby_guid: str) -> list[dict[str, Any]]:
    if not isinstance(sections, list):
        return []
    if not expected_emby_guid:
        return sections
    normalized: list[dict[str, Any]] = []
    for section in sections:
        if not isinstance(section, dict):
            continue
        next_section = dict(section)
        next_section["UserId"] = expected_emby_guid
        normalized.append(next_section)
    return normalized


def _parse_json_blob(blob: Any) -> dict | None:
    if blob is None:
        return None
    try:
        text = blob if isinstance(blob, str) else bytes(blob).decode("utf-8")
        return json.loads(text)
    except Exception:
        return None


def blob_to_emby_guid(blob: bytes | bytearray | memoryview | None) -> str:
    if not blob:
        return ""
    raw = bytes(blob)
    if len(raw) != 16:
        return raw.hex().lower()
    reordered = bytes(
        [
            raw[3], raw[2], raw[1], raw[0],
            raw[5], raw[4],
            raw[7], raw[6],
            raw[8], raw[9], raw[10], raw[11], raw[12], raw[13], raw[14], raw[15],
        ]
    )
    return reordered.hex().lower()


def _has_table(conn: sqlite3.Connection, table_name: str) -> bool:
    row = conn.execute("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", (table_name,)).fetchone()
    return bool(row)


def _users_table_columns(conn: sqlite3.Connection) -> list[str]:
    rows = conn.execute("PRAGMA table_info(Users)").fetchall()
    return [str(row[1]) for row in rows]


def _find_column(columns: list[str], *patterns: str) -> str | None:
    lower_map = {col.lower(): col for col in columns}
    for pattern in patterns:
        for lower, original in lower_map.items():
            if lower == pattern.lower():
                return original
    return None


def _load_users_table_users(conn: sqlite3.Connection) -> list[dict[str, Any]]:
    columns = _users_table_columns(conn)
    name_col = _find_column(columns, "Username", "Name")
    guid_col = _find_column(columns, "Guid")
    id_col = _find_column(columns, "Id") or "Id"
    if not name_col:
        raise RuntimeError(f"Cannot find a name column in Users table. Columns found: {', '.join(columns)}")
    select_cols = ", ".join([col for col in [id_col, name_col, guid_col] if col])
    rows = conn.execute(f"SELECT {select_cols} FROM Users").fetchall()
    users: list[dict[str, Any]] = []
    for row in rows:
        raw_id = row[id_col]
        raw_guid = row[guid_col] if guid_col else None
        emby_guid = ""
        guid = ""
        if raw_guid:
            if isinstance(raw_guid, (bytes, bytearray, memoryview)):
                buf = bytes(raw_guid)
                guid = buf.hex().upper()
                emby_guid = blob_to_emby_guid(buf)
            elif isinstance(raw_guid, str):
                clean = normalize_guid(raw_guid)
                emby_guid = clean
                guid = clean.upper()
        users.append(
            {
                "id": raw_id,
                "name": row[name_col] or f"User {raw_id}",
                "guid": guid,
                "embyGuid": emby_guid,
                "sourceTable": "Users",
            }
        )
    return users


def _load_local_users(conn: sqlite3.Connection) -> list[dict[str, Any]]:
    rows = conn.execute("SELECT Id, guid, data FROM LocalUsersv2").fetchall()
    users: list[dict[str, Any]] = []
    for row in rows:
        parsed = _parse_json_blob(row["data"])
        guid_blob = row["guid"]
        guid = bytes(guid_blob).hex().upper() if guid_blob else ""
        emby_guid_from_blob = blob_to_emby_guid(guid_blob)
        emby_guid_from_json = normalize_guid((parsed or {}).get("IdString"))
        emby_guid = emby_guid_from_json or emby_guid_from_blob
        users.append(
            {
                "id": row["Id"],
                "name": (parsed or {}).get("Name") or f"User {row['Id']}",
                "guid": guid,
                "embyGuid": emby_guid,
                "sourceTable": "LocalUsersv2",
                "profile": parsed,
            }
        )
    return users


def load_canonical_users(conn: sqlite3.Connection) -> list[dict[str, Any]]:
    if _has_table(conn, "LocalUsersv2"):
        return _load_local_users(conn)
    if _has_table(conn, "Users"):
        return _load_users_table_users(conn)
    raise RuntimeError("No supported user table found. Expected LocalUsersv2 or Users.")


def _home_screen_setting_rows(conn: sqlite3.Connection) -> list[sqlite3.Row]:
    return conn.execute(
        """
        SELECT us.UserId, us.Value
        FROM UserSettings us
        JOIN UserSettingsKeys usk ON us.UserSettingsKeyId = usk.UserSettingsKeyId
        WHERE usk.Name = 'homescreensettings'
        """
    ).fetchall()


def read_db(db_path: str) -> dict[str, Any]:
    path = Path(db_path)
    if not path.exists():
        raise FileNotFoundError(f"Database file not found: {path}")
    conn = sqlite3.connect(str(path))
    conn.row_factory = sqlite3.Row
    try:
        users = load_canonical_users(conn)
        settings_rows = _home_screen_setting_rows(conn)
        settings_map = {str(row["UserId"]): row["Value"] for row in settings_rows}
        user_ids = {str(user["id"]) for user in users}

        matched_users = 0
        mismatched_users = 0
        normalized_users = 0
        missing_section_user_ids = 0
        hydrated_users: list[dict[str, Any]] = []

        for user in users:
            raw_value = settings_map.get(str(user["id"]))
            sections: list[dict[str, Any]] = []
            try:
                if raw_value:
                    sections = (json.loads(raw_value) or {}).get("Sections") or []
            except Exception:
                sections = []

            actual_user_ids = sorted({normalize_guid(section.get("UserId")) for section in sections if normalize_guid(section.get("UserId"))})
            mismatched_section_user_ids = (
                [value for value in actual_user_ids if value != user["embyGuid"]]
                if user.get("embyGuid")
                else list(actual_user_ids)
            )
            missing_ids_for_user = sum(1 for section in sections if not normalize_guid(section.get("UserId")))
            normalized_sections = normalize_sections_for_user(sections, user.get("embyGuid", ""))
            sections_were_normalized = json.dumps(sections, sort_keys=True) != json.dumps(normalized_sections, sort_keys=True)

            if mismatched_section_user_ids:
                mismatched_users += 1
            else:
                matched_users += 1
            if sections_were_normalized:
                normalized_users += 1
            missing_section_user_ids += missing_ids_for_user

            profile = user.get("profile") or {}
            hydrated_users.append(
                {
                    "id": user["id"],
                    "name": user["name"],
                    "dbName": user["name"],
                    "guid": user.get("guid", ""),
                    "embyGuid": user.get("embyGuid", ""),
                    "embyName": None,
                    "sections": normalized_sections,
                    "details": {
                        "sourceTable": user["sourceTable"],
                        "lastLoginDate": profile.get("LastLoginDate"),
                        "lastActivityDate": profile.get("LastActivityDate"),
                        "usesIdForConfigurationPath": profile.get("UsesIdForConfigurationPath"),
                        "importedCollectionsCount": len(profile.get("ImportedCollections") or []) if isinstance(profile.get("ImportedCollections"), list) else 0,
                    },
                    "match": {
                        "sourceTable": user["sourceTable"],
                        "settingsUserId": user["id"],
                        "expectedSectionUserId": user.get("embyGuid", ""),
                        "actualSectionUserIds": actual_user_ids,
                        "mismatchedSectionUserIds": mismatched_section_user_ids,
                        "missingSectionUserIds": missing_ids_for_user,
                        "ok": not mismatched_section_user_ids,
                    },
                }
            )

        orphaned_settings_user_ids = sorted({str(row["UserId"]) for row in settings_rows if str(row["UserId"]) not in user_ids})
        return {
            "users": hydrated_users,
            "validation": {
                "userSource": users[0]["sourceTable"] if users else None,
                "userCount": len(hydrated_users),
                "settingsCount": len(settings_rows),
                "matchedUsers": matched_users,
                "mismatchedUsers": mismatched_users,
                "normalizedUsers": normalized_users,
                "missingSectionUserIds": missing_section_user_ids,
                "orphanedSettingsUserIds": orphaned_settings_user_ids,
            },
        }
    finally:
        conn.close()


def write_db(db_path: str, changes: list[dict[str, Any]]) -> dict[str, Any]:
    path = Path(db_path)
    if not path.exists():
        raise FileNotFoundError(f"Database file not found: {path}")
    conn = sqlite3.connect(str(path))
    conn.row_factory = sqlite3.Row
    try:
        user_lookup = {str(user["id"]): user for user in load_canonical_users(conn)}
        key_row = conn.execute(
            "SELECT UserSettingsKeyId FROM UserSettingsKeys WHERE Name = 'homescreensettings'"
        ).fetchone()
        if not key_row:
            raise RuntimeError("'homescreensettings' key not found in UserSettingsKeys table")
        key_id = key_row["UserSettingsKeyId"]
        count = 0
        normalized_sections = 0
        conn.execute("BEGIN")
        try:
            for change in changes:
                user_id = str(change.get("userId"))
                sections = change.get("sections")
                user = user_lookup.get(user_id)
                if not user:
                    raise RuntimeError(f"UserId {user_id} does not exist in {path}")
                next_sections = normalize_sections_for_user(sections, user.get("embyGuid", ""))
                if json.dumps(next_sections, sort_keys=True) != json.dumps(sections, sort_keys=True):
                    normalized_sections += len(next_sections)
                value = json.dumps({"Sections": next_sections}, separators=(",", ":"))
                exists = conn.execute(
                    "SELECT 1 FROM UserSettings WHERE UserId = ? AND UserSettingsKeyId = ?",
                    (change.get("userId"), key_id),
                ).fetchone()
                if exists:
                    conn.execute(
                        "UPDATE UserSettings SET Value = ? WHERE UserId = ? AND UserSettingsKeyId = ?",
                        (value, change.get("userId"), key_id),
                    )
                else:
                    conn.execute(
                        "INSERT INTO UserSettings (UserId, UserSettingsKeyId, Value) VALUES (?, ?, ?)",
                        (change.get("userId"), key_id, value),
                    )
                count += 1
            conn.commit()
        except Exception:
            conn.rollback()
            raise
        return {"ok": True, "count": count, "normalizedSections": normalized_sections}
    finally:
        conn.close()


def generate_sql(users: list[dict[str, Any]], original_users: list[dict[str, Any]]) -> str:
    statements: list[str] = []
    original_lookup = {str(user.get("id")): user for user in original_users}
    for user in users:
        sections = user.get("sections")
        if not isinstance(sections, list):
            continue
        original = original_lookup.get(str(user.get("id")))
        if not original:
            continue
        orig_json = json.dumps({"Sections": original.get("sections") or []}, separators=(",", ":"))
        new_json = json.dumps({"Sections": sections}, separators=(",", ":"))
        if orig_json == new_json:
            continue
        escaped_value = new_json.replace("'", "''")
        statements.extend(
            [
                f"-- User: {user.get('name', 'Unknown')} (DB ID: {user.get('id')})",
                "UPDATE UserSettings "
                f"SET Value = '{escaped_value}' "
                f"WHERE UserId = {user.get('id')} "
                "AND UserSettingsKeyId = "
                "(SELECT UserSettingsKeyId FROM UserSettingsKeys WHERE Name = 'homescreensettings');",
                "",
            ]
        )
    if not statements:
        return "-- No changes detected"
    header = [
        "-- ===========================================",
        "-- Emby Home Screen Settings Update",
        f"-- Generated: {datetime.now().astimezone().isoformat(timespec='seconds')}",
        "-- ===========================================",
        "-- IMPORTANT: Stop Emby before running this!",
        "-- sqlite3 /path/to/users.db < this_file.sql",
        "-- Then restart Emby.",
        "-- ===========================================",
        "",
        "BEGIN TRANSACTION;",
        "",
    ]
    return "\n".join(header + statements + ["COMMIT;"])


def _read_json_cache(path: Path, default: Any) -> Any:
    if not path.exists():
        return default
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return default


def _write_json_cache(path: Path, payload: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2), encoding="utf-8")


def _iso_now() -> str:
    return datetime.now().astimezone().isoformat(timespec="seconds")


def _upload_path(upload_id: str) -> Path:
    safe_id = "".join(ch for ch in str(upload_id or "") if ch.isalnum() or ch in ("-", "_")).strip()
    if not safe_id:
        raise ValueError("Invalid upload id.")
    return HOMESCREEN_UPLOAD_DIR / f"{safe_id}.db"


def get_active_upload() -> dict[str, Any] | None:
    payload = _read_json_cache(HOMESCREEN_UPLOAD_STATE_PATH, {})
    if not isinstance(payload, dict):
        return None
    upload_id = str(payload.get("upload_id") or "").strip()
    if not upload_id:
        return None
    path = _upload_path(upload_id)
    if not path.exists():
        return None
    return {
        "upload_id": upload_id,
        "filename": str(payload.get("filename") or path.name),
        "size_bytes": int(payload.get("size_bytes") or path.stat().st_size),
        "uploaded_at": payload.get("uploaded_at"),
        "sha256": str(payload.get("sha256") or ""),
        "path": str(path),
    }


def save_uploaded_db(filename: str, content: bytes) -> dict[str, Any]:
    if not content:
        raise ValueError("Uploaded file is empty.")
    HOMESCREEN_UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
    upload_id = uuid.uuid4().hex
    path = _upload_path(upload_id)
    path.write_bytes(content)
    meta = {
        "upload_id": upload_id,
        "filename": Path(filename or "users.db").name or "users.db",
        "size_bytes": len(content),
        "uploaded_at": _iso_now(),
        "sha256": hashlib.sha256(content).hexdigest(),
    }
    _write_json_cache(HOMESCREEN_UPLOAD_STATE_PATH, meta)
    return {**meta, "path": str(path)}


def resolve_db_source(db_path: str | None = None, upload_id: str | None = None) -> tuple[str, dict[str, Any] | None]:
    if upload_id:
        path = _upload_path(upload_id)
        if not path.exists():
            raise FileNotFoundError(f"Uploaded database not found for id {upload_id}.")
        active = get_active_upload()
        if active and active.get("upload_id") == upload_id:
            return str(path), active
        stat = path.stat()
        return str(path), {
            "upload_id": upload_id,
            "filename": path.name,
            "size_bytes": stat.st_size,
            "uploaded_at": None,
            "sha256": "",
            "path": str(path),
        }
    active = get_active_upload()
    if active:
        return active["path"], active
    if db_path:
        return db_path, None
    raise FileNotFoundError("No homescreen database source configured.")


def read_cached_emby_users() -> dict[str, Any]:
    payload = _read_json_cache(EMBY_USERS_CACHE_PATH, {"users": [], "lastSyncedAt": None})
    users = payload.get("users") if isinstance(payload, dict) else []
    last_synced = payload.get("lastSyncedAt") if isinstance(payload, dict) else None
    normalized = [
        {"embyGuid": normalize_guid(user.get("embyGuid")), "name": str(user.get("name") or "").strip()}
        for user in users
        if normalize_guid(user.get("embyGuid")) and str(user.get("name") or "").strip()
    ]
    normalized.sort(key=lambda user: (user["name"].lower(), user["embyGuid"]))
    return {"users": normalized, "lastSyncedAt": last_synced}


def write_cached_emby_users(users: list[dict[str, Any]]) -> dict[str, Any]:
    fetched_at = datetime.now().astimezone().isoformat(timespec="seconds")
    payload = {
        "users": [
            {"embyGuid": normalize_guid(user.get("embyGuid")), "name": str(user.get("name") or "").strip()}
            for user in users
            if normalize_guid(user.get("embyGuid")) and str(user.get("name") or "").strip()
        ],
        "lastSyncedAt": fetched_at,
    }
    payload["users"].sort(key=lambda user: (user["name"].lower(), user["embyGuid"]))
    _write_json_cache(EMBY_USERS_CACHE_PATH, payload)
    return payload


def apply_cached_emby_names(users: list[dict[str, Any]]) -> dict[str, Any]:
    cached = read_cached_emby_users()
    lookup = {user["embyGuid"]: user for user in cached["users"]}
    enriched = []
    matched = 0
    for user in users:
        emby_guid = normalize_guid(user.get("embyGuid"))
        cached_user = lookup.get(emby_guid)
        if cached_user:
            matched += 1
        enriched.append(
            {
                **user,
                "dbName": user.get("dbName") or user.get("name"),
                "embyName": cached_user["name"] if cached_user else user.get("embyName"),
                "name": cached_user["name"] if cached_user else user.get("name"),
            }
        )
    return {
        "users": enriched,
        "cache": {
            "matchedCount": matched,
            "totalCachedUsers": len(cached["users"]),
            "lastSyncedAt": cached.get("lastSyncedAt"),
        },
    }


def read_cached_user_context(emby_guid: str) -> dict[str, Any] | None:
    payload = _read_json_cache(EMBY_USER_CONTEXT_CACHE_PATH, {})
    return payload.get(normalize_guid(emby_guid)) if isinstance(payload, dict) else None


def write_cached_user_context(emby_guid: str, context: dict[str, Any]) -> dict[str, Any]:
    payload = _read_json_cache(EMBY_USER_CONTEXT_CACHE_PATH, {})
    normalized_guid = normalize_guid(emby_guid)
    payload[normalized_guid] = context
    _write_json_cache(EMBY_USER_CONTEXT_CACHE_PATH, payload)
    return context
