"""Immich action executor — implements auto_organize and future action types."""

from __future__ import annotations

import logging
from typing import Any

from ..action_executor import ActionExecutor, ActionResult, RuleResult
from .client import ImmichApiError, ImmichClient

_LOGGER = logging.getLogger(__name__)


class ImmichActionExecutor(ActionExecutor):
    """Executes actions against an Immich server."""

    def __init__(self, client: ImmichClient) -> None:
        self._client = client

    async def execute(
        self,
        action_type: str,
        rules: list[dict[str, Any]],
        config: dict[str, Any],
    ) -> ActionResult:
        if action_type == "auto_organize":
            return await self._execute_auto_organize(rules, config, dry_run=False)
        return ActionResult(success=False, error=f"Unknown action type: {action_type}")

    async def validate_rules(
        self,
        action_type: str,
        rules: list[dict[str, Any]],
    ) -> list[str]:
        errors: list[str] = []
        if action_type == "auto_organize":
            for i, rule in enumerate(rules):
                criteria = rule.get("criteria", {})
                if not criteria:
                    errors.append(f"Rule {i + 1}: criteria is required")
                target = rule.get("target_album_id", "")
                create = rule.get("create_album_if_missing", False)
                if not target and not create:
                    errors.append(
                        f"Rule {i + 1}: target_album_id is required "
                        "unless create_album_if_missing is true"
                    )
                if create and not rule.get("create_album_name"):
                    errors.append(
                        f"Rule {i + 1}: create_album_name is required "
                        "when create_album_if_missing is true"
                    )
                # Must have at least one criteria source
                has_source = bool(
                    criteria.get("person_ids")
                    or criteria.get("query")
                )
                if not has_source:
                    errors.append(
                        f"Rule {i + 1}: criteria must include "
                        "at least person_ids or query"
                    )
        else:
            errors.append(f"Unknown action type: {action_type}")
        return errors

    async def dry_run(
        self,
        action_type: str,
        rules: list[dict[str, Any]],
        config: dict[str, Any],
    ) -> ActionResult:
        if action_type == "auto_organize":
            return await self._execute_auto_organize(rules, config, dry_run=True)
        return ActionResult(success=False, error=f"Unknown action type: {action_type}")

    # ------------------------------------------------------------------
    # auto_organize implementation
    # ------------------------------------------------------------------

    async def _execute_auto_organize(
        self,
        rules: list[dict[str, Any]],
        config: dict[str, Any],
        *,
        dry_run: bool,
    ) -> ActionResult:
        rule_results: list[RuleResult] = []
        total_affected = 0

        for rule in rules:
            result = await self._execute_single_organize_rule(rule, dry_run=dry_run)
            rule_results.append(result)
            if result.success:
                total_affected += result.items_affected

        succeeded = sum(1 for r in rule_results if r.success)
        failed = len(rule_results) - succeeded

        if failed == 0:
            status = True
        elif succeeded == 0:
            status = False
        else:
            status = True  # partial success is still "success" at action level

        return ActionResult(
            success=status,
            rules_processed=len(rule_results),
            rules_succeeded=succeeded,
            rules_failed=failed,
            total_items_affected=total_affected,
            rule_results=rule_results,
        )

    async def _execute_single_organize_rule(
        self,
        rule: dict[str, Any],
        *,
        dry_run: bool,
    ) -> RuleResult:
        rule_name = rule.get("name", rule.get("target_album_name", "unnamed"))
        criteria = rule.get("criteria", {})
        create_if_missing = rule.get("create_album_if_missing", False)
        create_album_name = rule.get("create_album_name", "")

        # Support both target_album_ids (array) and target_album_id (single, backward compat)
        target_album_ids: list[str] = list(rule.get("target_album_ids") or [])
        if not target_album_ids:
            single = rule.get("target_album_id", "")
            if single:
                target_album_ids = [single]

        try:
            # Step 1: Gather candidate assets from criteria.  Asset type is
            # kept alongside the id so we can pick the first *photo* (not a
            # video) as an album thumbnail when one is missing.
            candidate_ids, types_by_id = await self._gather_candidates(criteria)

            if not candidate_ids:
                return RuleResult(
                    rule_name=rule_name,
                    success=True,
                    items_matched=0,
                    items_affected=0,
                    items_skipped=0,
                    details={"message": "No assets matched criteria"},
                )

            # If no target albums and create_if_missing, create one
            album_created_now: set[str] = set()
            if not target_album_ids and create_if_missing and create_album_name:
                if dry_run:
                    _LOGGER.info("[DRY RUN] Would create album '%s'", create_album_name)
                    target_album_ids = ["__dry_run_new__"]
                else:
                    created = await self._client.create_album(create_album_name)
                    target_album_ids = [created.get("id", "")]
                    if target_album_ids[0]:
                        album_created_now.add(target_album_ids[0])
                    _LOGGER.info("Created album '%s' with id %s", create_album_name, target_album_ids[0])

            if not target_album_ids:
                return RuleResult(
                    rule_name=rule_name,
                    success=False,
                    error="No target albums specified",
                )

            # Step 2-4: For each target album, diff and add
            total_affected = 0
            total_skipped = 0
            album_details: list[dict[str, Any]] = []

            for album_id in target_album_ids:
                album_asset_ids: set[str] = set()
                needs_thumbnail = album_id in album_created_now

                if album_id and album_id != "__dry_run_new__":
                    # Actions diff the current album state to decide what to
                    # add — must observe fresh data, not a cached view.
                    album = await self._client.get_album(album_id, use_cache=False)
                    if album is None and create_if_missing and create_album_name:
                        if not dry_run:
                            created = await self._client.create_album(create_album_name)
                            album_id = created.get("id", album_id)
                            album_created_now.add(album_id)
                            needs_thumbnail = True
                            _LOGGER.info("Created album '%s' with id %s", create_album_name, album_id)
                    elif album is None:
                        album_details.append({"album_id": album_id, "error": "not found"})
                        continue
                    elif album is not None:
                        album_asset_ids = set(album.asset_ids)
                        if not album.thumbnail_asset_id:
                            needs_thumbnail = True

                new_asset_ids = [aid for aid in candidate_ids if aid not in album_asset_ids]
                skipped = len(candidate_ids) - len(new_asset_ids)

                thumbnail_set_id: str | None = None
                if new_asset_ids and not dry_run and album_id:
                    for i in range(0, len(new_asset_ids), 500):
                        batch = new_asset_ids[i : i + 500]
                        await self._client.add_assets_to_album(album_id, batch)
                    _LOGGER.info("Added %d assets to album %s", len(new_asset_ids), album_id)

                    # Best-effort: give newly-created/empty-thumbnail albums a
                    # cover.  Prefer the first image; fall back to the first
                    # added asset of any type if none are images (Immich renders
                    # a video poster, which still looks fine).  Failures here
                    # must not fail the rule — the add already succeeded.
                    if needs_thumbnail:
                        pick = next(
                            (aid for aid in new_asset_ids if (types_by_id.get(aid) or "").lower() == "image"),
                            None,
                        ) or new_asset_ids[0]
                        try:
                            await self._client.set_album_thumbnail(album_id, pick)
                            thumbnail_set_id = pick
                            _LOGGER.info("Set thumbnail of album %s to %s", album_id, pick)
                        except ImmichApiError as err:
                            _LOGGER.warning(
                                "Could not set thumbnail for album %s: %s", album_id, err
                            )
                elif dry_run and new_asset_ids:
                    _LOGGER.info("[DRY RUN] Would add %d assets to album %s", len(new_asset_ids), album_id)
                    if needs_thumbnail:
                        _LOGGER.info("[DRY RUN] Would set album %s thumbnail to first added asset", album_id)

                total_affected += len(new_asset_ids)
                total_skipped += skipped
                detail = {"album_id": album_id, "added": len(new_asset_ids), "skipped": skipped}
                if thumbnail_set_id:
                    detail["thumbnail_set_to"] = thumbnail_set_id
                album_details.append(detail)

            return RuleResult(
                rule_name=rule_name,
                success=True,
                items_matched=len(candidate_ids),
                items_affected=total_affected,
                items_skipped=total_skipped,
                details={
                    "target_album_ids": target_album_ids,
                    "albums": album_details,
                    "dry_run": dry_run,
                },
            )

        except ImmichApiError as err:
            _LOGGER.error("Rule '%s' failed: %s", rule_name, err)
            return RuleResult(
                rule_name=rule_name,
                success=False,
                error=str(err),
            )
        except Exception as err:
            _LOGGER.error("Unexpected error in rule '%s': %s", rule_name, err)
            return RuleResult(
                rule_name=rule_name,
                success=False,
                error=f"Unexpected error: {err}",
            )

    async def _gather_candidates(
        self, criteria: dict[str, Any]
    ) -> tuple[list[str], dict[str, str]]:
        """Gather asset IDs matching the criteria (union of all sources).

        Returns ``(ordered_ids, types_by_id)`` so callers that need asset
        type — e.g. picking a photo for an album thumbnail — don't have to
        re-fetch each asset.
        """
        seen: set[str] = set()
        result: list[str] = []
        types_by_id: dict[str, str] = {}

        # Source 1: Person assets
        person_ids = criteria.get("person_ids", [])
        for pid in person_ids:
            assets = await self._client.get_person_assets_all(pid)
            for asset in assets:
                aid = asset.get("id", "")
                if aid and aid not in seen:
                    if self._matches_filters(asset, criteria):
                        seen.add(aid)
                        result.append(aid)
                        types_by_id[aid] = asset.get("type", "") or ""

        # Source 2: Smart search
        query = criteria.get("query", "")
        if query:
            assets = await self._client.search_smart_all(query)
            for asset in assets:
                aid = asset.get("id", "")
                if aid and aid not in seen:
                    if self._matches_filters(asset, criteria):
                        seen.add(aid)
                        result.append(aid)
                        types_by_id[aid] = asset.get("type", "") or ""

        # Exclude assets belonging to excluded persons
        exclude_person_ids = criteria.get("exclude_person_ids", [])
        if exclude_person_ids:
            excluded_asset_ids: set[str] = set()
            for pid in exclude_person_ids:
                assets = await self._client.get_person_assets_all(pid)
                for asset in assets:
                    aid = asset.get("id", "")
                    if aid:
                        excluded_asset_ids.add(aid)
            result = [aid for aid in result if aid not in excluded_asset_ids]
            for aid in list(types_by_id):
                if aid not in excluded_asset_ids:
                    continue
                types_by_id.pop(aid, None)

        return result, types_by_id

    def _matches_filters(
        self, asset: dict[str, Any], criteria: dict[str, Any]
    ) -> bool:
        """Apply client-side filters (asset_type, date range, favorites)."""
        # Asset type filter
        asset_type_filter = criteria.get("asset_type", "all")
        if asset_type_filter != "all":
            asset_type = (asset.get("type") or "").lower()
            if asset_type_filter == "image" and asset_type != "image":
                return False
            if asset_type_filter == "video" and asset_type != "video":
                return False

        # Favorite filter
        if criteria.get("favorite_only") and not asset.get("isFavorite"):
            return False

        # Date range filter
        date_from = criteria.get("date_from")
        date_to = criteria.get("date_to")
        if date_from or date_to:
            created = asset.get("fileCreatedAt") or asset.get("createdAt") or ""
            if created:
                try:
                    asset_date = created[:10]  # "YYYY-MM-DD"
                    if date_from and asset_date < date_from:
                        return False
                    if date_to and asset_date > date_to:
                        return False
                except (ValueError, IndexError):
                    pass

        return True
