"""Orchestration for the User Favourites feature.

Combines the user / collection / watch-history / recommendation services into the
operations the API exposes: list users, browse collections, view a collection's
items, clean up watched items, and regenerate recommendations. Dry-run is the
default for both destructive (cleanup) and bulk (regenerate) actions, and the
destructive actions only run on a user's own ``"{Name} Favorites"`` collection.
"""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from pathlib import Path

from . import emby_collections, emby_users, emby_watch_history, recommendations
from .recommendations import DEFAULT_TARGET_SIZE

LOG_DIR = Path("logs")
_logger: logging.Logger | None = None


class FavoritesError(Exception):
    """Raised for expected, user-facing problems (missing user/collection, etc.).

    Carries an HTTP-ish ``status`` so the route layer can map it cleanly.
    """

    def __init__(self, message: str, status: int = 400):
        super().__init__(message)
        self.message = message
        self.status = status


def _get_logger() -> logging.Logger:
    """Lazily configure a dedicated favourites logger with a file handler.

    Guards against duplicate handlers across uvicorn reloads.
    """
    global _logger
    if _logger is not None:
        return _logger
    log = logging.getLogger("homelabtoolkit.favorites")
    log.setLevel(logging.INFO)
    if not any(isinstance(h, logging.FileHandler) for h in log.handlers):
        try:
            LOG_DIR.mkdir(exist_ok=True)
            handler = logging.FileHandler(LOG_DIR / "favorites.log", encoding="utf-8")
            handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
            log.addHandler(handler)
        except OSError:
            # Filesystem unavailable (read-only container): fall back to console.
            pass
    _logger = log
    return log


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def _log_action(user_name, collection_name, item, reason, action):
    record = {
        "timestamp": _now_iso(),
        "action": action,
        "user": user_name,
        "collection": collection_name,
        "title": item.get("title", ""),
        "item_id": item.get("id", ""),
        "reason": reason,
    }
    _get_logger().info(
        "%s | user=%s | collection=%s | item=%s (%s) | reason=%s",
        action, user_name, collection_name, record["title"], record["item_id"], reason,
    )
    return record


async def _resolve_collection(client, collection_id: str, user_id: str) -> tuple[dict, dict]:
    """Resolve ``(user, collection)`` by id or raise :class:`FavoritesError`."""
    user = await emby_users.get_user(client, user_id)
    if not user:
        raise FavoritesError(f"No Emby user found for id {user_id!r}.", status=404)
    collection = next(
        (c for c in await emby_collections.find_all_collections(client) if c["collection_id"] == collection_id),
        None,
    )
    if not collection:
        raise FavoritesError(f"No collection found for id {collection_id!r}.", status=404)
    return user, collection


async def list_favorites_users(client) -> list[dict]:
    """Users that have a detected ``"{Name} Favorites"`` collection."""
    users = await emby_users.fetch_users(client)
    by_name = {u["name"].casefold(): u for u in users}
    result = []
    for collection in await emby_collections.find_favorites_collections(client):
        user = by_name.get(collection["owner_name"].casefold())
        if not user:
            continue  # orphan collection with no matching user
        result.append(
            {
                "user_id": user["id"],
                "user_name": user["name"],
                "collection_id": collection["collection_id"],
                "collection_name": collection["collection_name"],
                "item_count": collection.get("item_count"),
            }
        )
    result.sort(key=lambda r: r["user_name"].casefold())
    return result


async def list_collections_overview(client) -> dict:
    """All collections plus all users, for the browse pickers.

    Each collection is annotated with ``owner_user_id`` when its ``"{Name}
    Favorites"`` owner resolves to a real Emby user.
    """
    users = await emby_users.fetch_users(client)
    by_name = {u["name"].casefold(): u for u in users}
    collections = []
    for collection in await emby_collections.find_all_collections(client):
        owner = by_name.get(collection["owner_name"].casefold()) if collection["owner_name"] else None
        collections.append({**collection, "owner_user_id": owner["id"] if owner else None})
    return {"collections": collections, "users": users}


async def get_collection_items_view(client, collection_id: str, user_id: str) -> dict:
    """View any collection's items with watched status resolved for ``user_id``.

    Watched status is user-specific. ``actions_enabled`` is true only when the
    collection is a ``"{Name} Favorites"`` collection and the selected user is its
    owner, so cleanup/regenerate never touch shared or themed collections.
    """
    user = await emby_users.get_user(client, user_id)
    if not user:
        raise FavoritesError(f"No Emby user found for id {user_id!r}.", status=404)

    collection = next(
        (c for c in await emby_collections.find_all_collections(client) if c["collection_id"] == collection_id),
        None,
    )
    if not collection:
        raise FavoritesError(f"No collection found for id {collection_id!r}.", status=404)

    items = await emby_collections.list_collection_items(client, collection_id, user_id)
    watched_count = sum(1 for i in items if i["watched"])

    # Cleanup/regenerate are available for any collection. They act on the
    # selected user's watch data; removal affects the shared collection itself.
    actions_enabled = True
    actions_reason = ""

    return {
        "collection_id": collection_id,
        "collection_name": collection["collection_name"],
        "is_favorites": collection["is_favorites"],
        "owner_name": collection["owner_name"],
        "user_id": user["id"],
        "user_name": user["name"],
        "actions_enabled": actions_enabled,
        "actions_reason": actions_reason,
        "items": items,
        "summary": {
            "current_count": len(items),
            "watched_count": watched_count,
            "unwatched_count": len(items) - watched_count,
        },
    }


async def cleanup_watched(client, collection_id: str, user_id: str, dry_run: bool = True) -> dict:
    """Preview or remove items the selected user has already watched.

    Which items are "watched" is resolved per user (via ``UserData.Played`` for
    this user only). The removal itself operates on the collection, which is
    shared, so removed items leave the collection for everyone.
    """
    user, collection = await _resolve_collection(client, collection_id, user_id)
    items = await emby_collections.list_collection_items(client, collection_id, user_id)

    watched = [i for i in items if i["watched"]]

    records = []
    applied = False
    if not dry_run and watched:
        await emby_collections.remove_collection_items(
            client, collection["collection_id"], [i["id"] for i in watched]
        )
        applied = True
        records = [
            _log_action(user["name"], collection["collection_name"], i, "watched-by-user", "removed")
            for i in watched
        ]

    final_count = len(items) - (len(watched) if applied else 0)
    return {
        "dry_run": dry_run,
        "applied": applied,
        "user_id": user["id"],
        "user_name": user["name"],
        "collection_name": collection["collection_name"],
        "watched_found": len(watched),
        "removed": [
            {"title": i["title"], "item_id": i["id"], "reason": "watched-by-user"}
            for i in watched
        ],
        "log": records,
        "summary": {
            "current_count": len(items),
            "watched_count": len(watched),
            "removed_count": len(watched) if applied else 0,
            "final_count": final_count,
        },
    }


async def regenerate(
    client,
    collection_id: str,
    user_id: str,
    dry_run: bool = True,
    target_size: int = DEFAULT_TARGET_SIZE,
) -> dict:
    """Preview or add recommendations derived from the selected user's history.

    Candidates exclude items the user has watched and items already in the
    collection, and are never watched items. New items are added until the
    collection reaches ``target_size``.
    """
    if target_size < 0:
        raise FavoritesError("targetSize must be zero or greater.", status=400)

    user, collection = await _resolve_collection(client, collection_id, user_id)
    items = await emby_collections.list_collection_items(client, collection_id, user_id)
    current_ids = {i["id"] for i in items if i["id"]}

    watched_items = await emby_watch_history.get_watched_items(client, user_id)
    watched_ids = {i["id"] for i in watched_items if i["id"]}
    profile = recommendations.build_profile(watched_items)

    summary_base = {
        "current_count": len(items),
        "watched_history_count": len(watched_items),
        "target_size": target_size,
    }

    if profile.is_empty:
        return {
            "dry_run": dry_run,
            "applied": False,
            "user_id": user["id"],
            "user_name": user["name"],
            "collection_name": collection["collection_name"],
            "message": "No watch history for this user yet, so no recommendations can be made.",
            "recommended": [],
            "log": [],
            "summary": {**summary_base, "recommended_count": 0, "added_count": 0, "final_count": len(items)},
        }

    exclude_ids = current_ids | watched_ids
    candidates = await recommendations.build_candidates(client, user_id, profile, exclude_ids)
    ranked = recommendations.rank_candidates(candidates, profile)

    need = max(0, target_size - len(items))
    chosen = ranked[:need]

    records = []
    applied = False
    if not dry_run and chosen:
        await emby_collections.add_collection_items(
            client, collection["collection_id"], [c["id"] for c in chosen]
        )
        applied = True
        records = [
            _log_action(
                user["name"], collection["collection_name"], c,
                f"recommended (score={c['score']})", "added",
            )
            for c in chosen
        ]

    final_count = len(items) + (len(chosen) if applied else 0)
    return {
        "dry_run": dry_run,
        "applied": applied,
        "user_id": user["id"],
        "user_name": user["name"],
        "collection_name": collection["collection_name"],
        "recommended": [
            {
                "title": c["title"],
                "item_id": c["id"],
                "type": c["type"],
                "year": c["year"],
                "runtime_minutes": c["runtime_minutes"],
                "community_rating": c["community_rating"],
                "score": c["score"],
            }
            for c in chosen
        ],
        "log": records,
        "summary": {
            **summary_base,
            "recommended_count": len(chosen),
            "added_count": len(chosen) if applied else 0,
            "final_count": final_count,
        },
    }
