# #region ResourceServiceModule [C:5] [TYPE Module] [SEMANTICS resource, git, task, status, superset]
# @BRIEF Shared service for fetching resource data with Git status and task status
# @LAYER Service
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [TaskManagerPackage]
# @RELATION DEPENDS_ON -> [TaskManagerModels]
# @RELATION DEPENDS_ON -> [GitService]
# @INVARIANT  All resources include metadata about their current state
# @SIDE_EFFECT  Queries multiple backends for status
# @DATA_CONTRACT  ResourceQuery -> ResourceStatusSummary

import asyncio
from datetime import UTC, datetime
from typing import Any

from ..core.logger import belief_scope, logger
from ..core.superset_client import SupersetClient
from ..core.task_manager.models import Task
from ..services.git_service import GitService


# #region ResourceService [C:3] [TYPE Class]
# @BRIEF Provides centralized access to resource data with enhanced metadata
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [GitService]
class ResourceService:

    # region ResourceService_init [TYPE Function]
    # @PURPOSE: Initialize the resource service with dependencies
    # @PRE  None
    # @POST  ResourceService is ready to fetch resources
    def __init__(self):
        with belief_scope("ResourceService.__init__"):
            self.git_service = GitService()
            logger.reason("Initialized ResourceService", extra={"src": "ResourceService"})
    # endregion ResourceService_init

    # region get_dashboards_with_status [TYPE Function]
    # @PURPOSE: Fetch dashboards from environment with Git status and last task status
    # @PRE  env is a valid Environment object
    # @POST  Returns list of dashboards with enhanced metadata
    # @PARAM  env (Environment) - The environment to fetch from
    # @PARAM  tasks (List[Task]) - List of tasks to check for status
    # @RETURN  List[Dict] - Dashboards with git_status and last_task fields
    # @RELATION CALLS -> [SupersetClientGetDashboardsSummary]
    # @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]
    # @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]
    async def get_dashboards_with_status(
        self,
        env: Any,
        tasks: list[Task] | None = None,
        include_git_status: bool = True,
        require_slug: bool = False,
    ) -> list[dict[str, Any]]:
        with belief_scope("get_dashboards_with_status", f"env={env.id}"):
            client = SupersetClient(env)
            dashboards = client.get_dashboards_summary(require_slug=require_slug)

            # Enhance each dashboard with Git status and task status
            result = []
            for dashboard in dashboards:
                # dashboard is already a dict, no need to call .dict()
                dashboard_dict = dashboard
                dashboard_id = dashboard_dict.get('id')

                # Git status can be skipped for list endpoints and loaded lazily on UI side.
                if include_git_status:
                    git_status = self._get_git_status_for_dashboard(dashboard_id)
                    dashboard_dict['git_status'] = git_status
                else:
                    dashboard_dict['git_status'] = None

                # Show status of the latest LLM validation for this dashboard.
                last_task = self._get_last_llm_task_for_dashboard(
                    dashboard_id,
                    env.id,
                    tasks,
                )
                dashboard_dict['last_task'] = last_task

                result.append(dashboard_dict)

            logger.info(f"[ResourceService][Coherence:OK] Fetched {len(result)} dashboards with status")
            return result
    # endregion get_dashboards_with_status

    # region get_dashboards_page_with_status [TYPE Function]
    # @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.
    # @PRE  env is valid; page >= 1; page_size > 0.
    # @POST  Returns page items plus total counters without scanning all pages locally.
    # @PARAM  env (Environment) - Source environment.
    # @PARAM  tasks (Optional[List[Task]]) - Tasks for latest LLM status.
    # @PARAM  page (int) - 1-based page number.
    # @PARAM  page_size (int) - Page size.
    # @RETURN  Dict[str, Any] - {"dashboards": List[Dict], "total": int, "total_pages": int}
    # @RELATION CALLS -> [SupersetClientGetDashboardsSummaryPage]
    # @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]
    # @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]
    async def get_dashboards_page_with_status(
        self,
        env: Any,
        tasks: list[Task] | None = None,
        page: int = 1,
        page_size: int = 10,
        search: str | None = None,
        include_git_status: bool = True,
        require_slug: bool = False,
    ) -> dict[str, Any]:
        with belief_scope(
            "get_dashboards_page_with_status",
            f"env={env.id}, page={page}, page_size={page_size}, search={search}",
        ):
            client = SupersetClient(env)
            total, dashboards_page = client.get_dashboards_summary_page(
                page=page,
                page_size=page_size,
                search=search,
                require_slug=require_slug,
            )

            result = []
            for dashboard in dashboards_page:
                dashboard_dict = dashboard
                dashboard_id = dashboard_dict.get("id")

                if include_git_status:
                    dashboard_dict["git_status"] = self._get_git_status_for_dashboard(dashboard_id)
                else:
                    dashboard_dict["git_status"] = None

                dashboard_dict["last_task"] = self._get_last_llm_task_for_dashboard(
                    dashboard_id,
                    env.id,
                    tasks,
                )
                result.append(dashboard_dict)

            total_pages = (total + page_size - 1) // page_size if total > 0 else 1
            logger.info(
                "[ResourceService][Coherence:OK] Fetched dashboards page %s/%s (%s items, total=%s)",
                page,
                total_pages,
                len(result),
                total,
            )
            return {
                "dashboards": result,
                "total": total,
                "total_pages": total_pages,
            }
    # endregion get_dashboards_page_with_status

    # region _get_last_llm_task_for_dashboard [TYPE Function]
    # @PURPOSE: Get most recent LLM validation task for a dashboard in an environment
    # @PRE  dashboard_id is a valid integer identifier
    # @POST  Returns the newest llm_dashboard_validation task summary or None
    # @PARAM  dashboard_id (int) - The dashboard ID
    # @PARAM  env_id (Optional[str]) - Environment ID to match task params
    # @PARAM  tasks (Optional[List[Task]]) - List of tasks to search
    # @RETURN  Optional[Dict] - Task summary with task_id and status
    # @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]
    # @RELATION CALLS ->[EXT:method:_normalize_validation_status]
    # @RELATION CALLS ->[EXT:method:_normalize_task_status]
    def _get_last_llm_task_for_dashboard(
        self,
        dashboard_id: int,
        env_id: str | None,
        tasks: list[Task] | None = None,
    ) -> dict[str, Any] | None:
        if not tasks:
            return None

        dashboard_id_str = str(dashboard_id)
        matched_tasks = []

        for task in tasks:
            if getattr(task, "plugin_id", None) != "llm_dashboard_validation":
                continue

            params = getattr(task, "params", {}) or {}
            if str(params.get("dashboard_id")) != dashboard_id_str:
                continue

            if env_id is not None:
                task_env = params.get("environment_id") or params.get("env")
                if str(task_env) != str(env_id):
                    continue

            matched_tasks.append(task)

        if not matched_tasks:
            return None

        def _task_time(task_obj: Any) -> datetime:
            raw_time = (
                getattr(task_obj, "started_at", None)
                or getattr(task_obj, "finished_at", None)
                or getattr(task_obj, "created_at", None)
            )
            return self._normalize_datetime_for_compare(raw_time)

        projected_tasks = []
        for task in matched_tasks:
            raw_result = getattr(task, "result", None)
            validation_status = None
            if isinstance(raw_result, dict):
                validation_status = self._normalize_validation_status(raw_result.get("status"))
            projected_tasks.append(
                (
                    task,
                    validation_status,
                    _task_time(task),
                )
            )

        projected_tasks.sort(key=lambda item: item[2], reverse=True)
        latest_task, latest_validation_status, _ = projected_tasks[0]
        decisive_task = next(
            (
                item for item in projected_tasks
                if item[1] in {"PASS", "WARN", "FAIL"}
            ),
            None,
        )
        validation_status = latest_validation_status
        if validation_status == "UNKNOWN" and decisive_task is not None:
            validation_status = decisive_task[1]

        return {
            "task_id": str(getattr(latest_task, "id", "")),
            "status": self._normalize_task_status(getattr(latest_task, "status", "")),
            "validation_status": validation_status,
        }
    # endregion _get_last_llm_task_for_dashboard

    # region _normalize_task_status [TYPE Function]
    # @PURPOSE: Normalize task status to stable uppercase values for UI/API projections
    # @PRE  raw_status can be enum or string
    # @POST  Returns uppercase status without enum class prefix
    # @PARAM  raw_status (Any) - Raw task status object/value
    # @RETURN  str - Normalized status token
    # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]
    def _normalize_task_status(self, raw_status: Any) -> str:
        if raw_status is None:
            return ""
        value = getattr(raw_status, "value", raw_status)
        status_text = str(value).strip()
        if "." in status_text:
            status_text = status_text.split(".")[-1]
        return status_text.upper()
    # endregion _normalize_task_status

    # region _normalize_validation_status [TYPE Function]
    # @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN
    # @PRE  raw_status can be any scalar type
    # @POST  Returns normalized validation status token or None
    # @PARAM  raw_status (Any) - Raw validation status from task result
    # @RETURN  Optional[str] - PASS|FAIL|WARN|UNKNOWN
    # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]
    def _normalize_validation_status(self, raw_status: Any) -> str | None:
        if raw_status is None:
            return None
        status_text = str(raw_status).strip().upper()
        if status_text in {"PASS", "FAIL", "WARN"}:
            return status_text
        return "UNKNOWN"
    # endregion _normalize_validation_status

    # region _normalize_datetime_for_compare [TYPE Function]
    # @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.
    # @PRE  value may be datetime or any scalar.
    # @POST  Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.
    # @PARAM  value (Any) - Candidate datetime-like value.
    # @RETURN  datetime - UTC-aware comparable datetime.
    # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]
    # @RELATION CALLED_BY -> [EXT:method:_get_last_task_for_resource]
    def _normalize_datetime_for_compare(self, value: Any) -> datetime:
        if isinstance(value, datetime):
            if value.tzinfo is None:
                return value.replace(tzinfo=UTC)
            return value.astimezone(UTC)
        return datetime.min.replace(tzinfo=UTC)
    # endregion _normalize_datetime_for_compare

    # region get_datasets_with_status [TYPE Function]
    # @PURPOSE: Fetch datasets from environment with mapping progress and last task status
    # @PRE  env is a valid Environment object
    # @POST  Returns list of datasets with enhanced metadata
    # @PARAM  env (Environment) - The environment to fetch from
    # @PARAM  tasks (List[Task]) - List of tasks to check for status
    # @RETURN  List[Dict] - Datasets with mapped_fields, last_task and linked_dashboard_count fields
    # @RELATION CALLS -> [SupersetClientGetDatasetsSummary]
    # @RELATION CALLS -> [SupersetClientGetDatasetLinkedDashboardCount]
    # @RELATION CALLS ->[EXT:method:_get_last_task_for_resource]
    # @RATIONALE linked_dashboard_count was missing — get_datasets_summary() only returns id/table_name/schema/database
    # StatsBar showed linked_count=0 because the field was never populated in list endpoint.
    # Fix: fetch /dataset/{id}/related_objects for each dataset concurrently (semaphore=3, timeout=10s).
    # @REJECTED N+1 blocking calls rejected — would timeout at 26×1s. Semaphore + asyncio.to_thread + timeout prevents overload.
    async def get_datasets_with_status(
        self,
        env: Any,
        tasks: list[Task] | None = None
    ) -> list[dict[str, Any]]:
        with belief_scope("get_datasets_with_status", f"env={env.id}"):
            client = SupersetClient(env)
            datasets = client.get_datasets_summary()

            # Enhance each dataset with task status and linked dashboard count
            sem = asyncio.Semaphore(3)  # limit concurrent Superset calls

            async def fetch_linked_count(ds_id: int) -> int:
                async with sem:
                    try:
                        return await asyncio.wait_for(
                            asyncio.to_thread(
                                client.get_dataset_linked_dashboard_count, ds_id
                            ),
                            timeout=10.0,
                        )
                    except (TimeoutError, Exception):
                        logger.warning(
                            f"[EXT:method:get_datasets_with_status][Warning] "
                            f"Failed to fetch linked dashboard count for dataset {ds_id}"
                        )
                        return 0

            result = []
            linked_tasks = []
            for dataset in datasets:
                dataset_dict = dataset
                dataset_id = dataset_dict.get('id')

                # Get last task status
                last_task = self._get_last_task_for_resource(
                    f"dataset-{dataset_id}",
                    tasks
                )
                dataset_dict['last_task'] = last_task

                result.append(dataset_dict)
                linked_tasks.append(fetch_linked_count(dataset_id))

            # Fetch linked dashboard counts concurrently — FR-022
            if linked_tasks:
                linked_counts = await asyncio.gather(*linked_tasks)
                for ds, count in zip(result, linked_counts):
                    ds['linked_dashboard_count'] = count

            logger.info(
                f"[ResourceService][Coherence:OK] "
                f"Fetched {len(result)} datasets with status and linked counts"
            )
            return result
    # endregion get_datasets_with_status

    # region get_activity_summary [TYPE Function]
    # @PURPOSE: Get summary of active and recent tasks for the activity indicator
    # @PRE  tasks is a list of Task objects
    # @POST  Returns summary with active_count and recent_tasks
    # @PARAM  tasks (List[Task]) - List of tasks to summarize
    # @RETURN  Dict - Activity summary
    # @RELATION CALLS ->[EXT:method:_extract_resource_name_from_task]
    # @RELATION CALLS ->[EXT:method:_extract_resource_type_from_task]
    def get_activity_summary(self, tasks: list[Task]) -> dict[str, Any]:
        with belief_scope("get_activity_summary"):
            # Count active (RUNNING, WAITING_INPUT) tasks
            active_tasks = [
                t for t in tasks
                if t.status in ['RUNNING', 'WAITING_INPUT']
            ]

            # Get recent tasks (last 5)
            recent_tasks = sorted(
                tasks,
                key=lambda t: t.created_at,
                reverse=True
            )[:5]

            # Format recent tasks for frontend
            recent_tasks_formatted = []
            for task in recent_tasks:
                resource_name = self._extract_resource_name_from_task(task)
                recent_tasks_formatted.append({
                    'task_id': str(task.id),
                    'resource_name': resource_name,
                    'resource_type': self._extract_resource_type_from_task(task),
                    'status': task.status,
                    'started_at': task.created_at.isoformat() if task.created_at else None
                })

            return {
                'active_count': len(active_tasks),
                'recent_tasks': recent_tasks_formatted
            }
    # endregion get_activity_summary

    # region _get_git_status_for_dashboard [TYPE Function]
    # @PURPOSE: Get Git sync status for a dashboard
    # @PRE  dashboard_id is a valid integer
    # @POST  Returns git status or None if no repo exists
    # @PARAM  dashboard_id (int) - The dashboard ID
    # @RETURN  Optional[Dict] - Git status with branch and sync_status
    # @RELATION CALLS ->[EXT:method:get_repo]
    def _get_git_status_for_dashboard(self, dashboard_id: int) -> dict[str, Any] | None:
        try:
            repo = self.git_service.get_repo(dashboard_id)
            if not repo:
                return {
                    'branch': None,
                    'sync_status': 'NO_REPO',
                    'has_repo': False,
                    'has_changes_for_commit': False
                }

            # Check if there are uncommitted changes
            try:
                # Get current branch
                branch = repo.active_branch.name

                # Check for uncommitted changes
                is_dirty = repo.is_dirty()
                has_changes_for_commit = repo.is_dirty(untracked_files=True)

                # Check for unpushed commits
                unpushed = len(list(repo.iter_commits(f'{branch}@{{u}}..{branch}'))) if '@{u}' in str(repo.refs) else 0

                if is_dirty or unpushed > 0:
                    sync_status = 'DIFF'
                else:
                    sync_status = 'OK'

                return {
                    'branch': branch,
                    'sync_status': sync_status,
                    'has_repo': True,
                    'has_changes_for_commit': has_changes_for_commit
                }
            except Exception:
                logger.warning(f"[ResourceService][Warning] Failed to get git status for dashboard {dashboard_id}")
                return {
                    'branch': None,
                    'sync_status': 'ERROR',
                    'has_repo': True,
                    'has_changes_for_commit': False
                }
        except Exception:
            # No repo exists for this dashboard
            return {
                'branch': None,
                'sync_status': 'NO_REPO',
                'has_repo': False,
                'has_changes_for_commit': False
            }
    # endregion _get_git_status_for_dashboard

    # region _get_last_task_for_resource [TYPE Function]
    # @PURPOSE: Get the most recent task for a specific resource
    # @PRE  resource_id is a valid string
    # @POST  Returns task summary or None if no tasks found
    # @PARAM  resource_id (str) - The resource identifier (e.g., "dashboard-123")
    # @PARAM  tasks (Optional[List[Task]]) - List of tasks to search
    # @RETURN  Optional[Dict] - Task summary with task_id and status
    # @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]
    def _get_last_task_for_resource(
        self,
        resource_id: str,
        tasks: list[Task] | None = None
    ) -> dict[str, Any] | None:
        if not tasks:
            return None

        # Filter tasks for this resource
        resource_tasks = []
        for task in tasks:
            params = task.params or {}
            if params.get('resource_id') == resource_id:
                resource_tasks.append(task)

        if not resource_tasks:
            return None

        # Get most recent task with timezone-safe comparison.
        last_task = max(
            resource_tasks,
            key=lambda t: self._normalize_datetime_for_compare(getattr(t, "created_at", None)),
        )

        return {
            'task_id': str(last_task.id),
            'status': last_task.status
        }
    # endregion _get_last_task_for_resource

    # region _extract_resource_name_from_task [TYPE Function]
    # @PURPOSE: Extract resource name from task params
    # @PRE  task is a valid Task object
    # @POST  Returns resource name or task ID
    # @PARAM  task (Task) - The task to extract from
    # @RETURN  str - Resource name or fallback
    # @RELATION CALLED_BY -> [EXT:method:get_activity_summary]
    def _extract_resource_name_from_task(self, task: Task) -> str:
        params = task.params or {}
        return params.get('resource_name', f"Task {task.id}")
    # endregion _extract_resource_name_from_task

    # region _extract_resource_type_from_task [TYPE Function]
    # @PURPOSE: Extract resource type from task params
    # @PRE  task is a valid Task object
    # @POST  Returns resource type or 'unknown'
    # @PARAM  task (Task) - The task to extract from
    # @RETURN  str - Resource type
    # @RELATION CALLED_BY -> [EXT:method:get_activity_summary]
    def _extract_resource_type_from_task(self, task: Task) -> str:
        params = task.params or {}
        return params.get('resource_type', 'unknown')
    # endregion _extract_resource_type_from_task
# #endregion ResourceService
# #endregion ResourceServiceModule
