# #region DatasetsApi [C:5] [TYPE Module] [SEMANTICS fastapi, dataset, api, search, mapping, mapped-fields]
#
# @BRIEF API endpoints for the Dataset Hub - listing datasets with mapping progress
# @LAYER     API
# @RELATION DEPENDS_ON -> [AppDependencies]
# @RELATION DEPENDS_ON -> [ResourceService]
# @RELATION DEPENDS_ON -> [SupersetClient]
#
# @PRE SupersetClient is available; env_id is valid.
# @POST Returns dataset metadata with mapping status.
# @SIDE_EFFECT Reads from Superset API and task manager.
# @DATA_CONTRACT Input -> DatasetQuery, Output -> DatasetsResponse, DatasetDetailResponse
# @INVARIANT  All dataset responses include last_task metadata


import re

from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, ConfigDict, Field

from ...core.logger import belief_scope, logger
from ...core.superset_client import SupersetClient
from ...dependencies import get_config_manager, get_resource_service, get_task_manager, has_permission

router = APIRouter(prefix="/api/datasets", tags=["Datasets"])

# #region MappedFields [C:1] [TYPE DataClass]
# @BRIEF DTO for dataset mapping progress statistics
class MappedFields(BaseModel):
    total: int
    mapped: int
# #endregion MappedFields

# #region LastTask [C:1] [TYPE DataClass]
# @BRIEF DTO for the most recent task associated with a dataset
class LastTask(BaseModel):
    task_id: str | None = None
    status: str | None = Field(None, pattern="^RUNNING|SUCCESS|ERROR|WAITING_INPUT$")
# #endregion LastTask

# #region DatasetItem [C:1] [TYPE DataClass]
# @BRIEF Summary DTO for a dataset in the hub listing
class DatasetItem(BaseModel):
    id: int
    table_name: str
    schema_name: str = Field(..., alias="schema")
    database: str
    mapped_fields: MappedFields | None = None
    last_task: LastTask | None = None
    metric_count: int = 0

    model_config = ConfigDict(validate_by_name=True)
# #endregion DatasetItem

# #region LinkedDashboard [C:1] [TYPE DataClass]
# @BRIEF DTO for a dashboard linked to a dataset
class LinkedDashboard(BaseModel):
    id: int
    title: str
    slug: str | None = None
# #endregion LinkedDashboard

# #region DatasetColumn [C:1] [TYPE DataClass]
# @BRIEF DTO for a single dataset column's metadata
class DatasetColumn(BaseModel):
    id: int
    name: str
    type: str | None = None
    is_dttm: bool = False
    is_active: bool = True
    description: str | None = None
# #endregion DatasetColumn

# #region MetricItem [C:1] [TYPE DataClass]
# @BRIEF Pydantic DTO for a dataset metric — carries Superset metric metadata.
# @RELATION DEPENDS_ON -> [DatasetDetailResponse]
class MetricItem(BaseModel):
    id: int
    metric_name: str
    expression: str | None = None
    verbose_name: str | None = None
    description: str | None = None
    metric_type: str | None = None
# #endregion MetricItem

# #region DatasetDetailResponse [C:2] [TYPE DataClass]
# @BRIEF Detailed DTO for a dataset including columns, linked dashboards, and metrics.
# @RELATION DEPENDS_ON -> [MetricItem]
class DatasetDetailResponse(BaseModel):
    id: int
    table_name: str | None = None
    schema_name: str | None = Field(None, alias="schema")
    database: str
    description: str | None = None
    columns: list[DatasetColumn]
    column_count: int
    metrics: list[MetricItem] = []
    metric_count: int = 0
    sql: str | None = None
    linked_dashboards: list[LinkedDashboard]
    linked_dashboard_count: int
    is_sqllab_view: bool = False
    created_on: str | None = None
    changed_on: str | None = None

    model_config = ConfigDict(validate_by_name=True)
# #endregion DatasetDetailResponse

# #region StatsCounts [C:1] [TYPE DataClass]
# @BRIEF Aggregate statistics for the Stats Bar — computed from full dataset list.
class StatsCounts(BaseModel):
    total: int
    unmapped_count: int
    mapped_count: int
    linked_count: int
# #endregion StatsCounts

# #region DatasetsResponse [C:2] [TYPE DataClass]
# @BRIEF Paginated response DTO for dataset listings with stats.
# @RELATION DEPENDS_ON -> [StatsCounts]
class DatasetsResponse(BaseModel):
    datasets: list[DatasetItem]
    stats: StatsCounts
    total: int
    page: int
    page_size: int
    total_pages: int
# #endregion DatasetsResponse

# #region TaskResponse [C:1] [TYPE DataClass]
# @BRIEF Response DTO containing a task ID for tracking
class TaskResponse(BaseModel):
    task_id: str
# #endregion TaskResponse

# #region ColumnDescriptionUpdate [C:1] [TYPE DataClass]
# @BRIEF Request DTO for inline-edit of a column description.
class ColumnDescriptionUpdate(BaseModel):
    description: str
# #endregion ColumnDescriptionUpdate

# #region MetricDescriptionUpdate [C:1] [TYPE DataClass]
# @BRIEF Request DTO for inline-edit of a metric description.
class MetricDescriptionUpdate(BaseModel):
    description: str
# #endregion MetricDescriptionUpdate

# #region get_dataset_ids [C:4] [TYPE Function]
# @BRIEF Fetch list of all dataset IDs from a specific environment (without pagination)
# @PRE  env_id must be a valid environment ID
# @POST  Returns a list of all dataset IDs
# @RELATION CALLS -> [EXT:method:get_datasets_with_status]
@router.get("/ids")
async def get_dataset_ids(
    env_id: str,
    search: str | None = None,
    config_manager=Depends(get_config_manager),
    task_manager=Depends(get_task_manager),
    resource_service=Depends(get_resource_service),
    _ = Depends(has_permission("plugin:migration", "READ"))
):
    with belief_scope("get_dataset_ids", f"env_id={env_id}, search={search}"):
        # Validate environment exists
        environments = config_manager.get_environments()
        env = next((e for e in environments if e.id == env_id), None)
        if not env:
            logger.error(f"[get_dataset_ids][Coherence:Failed] Environment not found: {env_id}")
            raise HTTPException(status_code=404, detail="Environment not found")

        try:
            # Get all tasks for status lookup
            all_tasks = task_manager.get_all_tasks()

            # Fetch datasets with status using ResourceService
            datasets = await resource_service.get_datasets_with_status(env, all_tasks)

            # Apply search filter if provided
            if search:
                search_lower = search.lower()
                datasets = [
                    d for d in datasets
                    if search_lower in d.get('table_name', '').lower()
                ]

            # Extract and return just the IDs
            dataset_ids = [d['id'] for d in datasets]
            logger.info(f"[get_dataset_ids][Coherence:OK] Returning {len(dataset_ids)} dataset IDs")

            return {"dataset_ids": dataset_ids}

        except Exception as e:
            logger.error(f"[get_dataset_ids][Coherence:Failed] Failed to fetch dataset IDs: {e}")
            raise HTTPException(status_code=503, detail=f"Failed to fetch dataset IDs: {e!s}")
# #endregion get_dataset_ids

# #region get_datasets [C:4] [TYPE Function]
# @BRIEF Fetch list of datasets from a specific environment with mapping progress and stats.
# @PRE  env_id must be a valid environment ID
# @PRE  page must be >= 1 if provided
# @PRE  page_size must be between 1 and 100 if provided
# @POST  Returns a list of datasets with enhanced metadata, pagination info, and StatsCounts.
# @POST  Response includes pagination metadata (page, page_size, total, total_pages) and stats object.
# @RATIONALE Stats counts returned in the same response as datasets to avoid an extra API call for the Stats Bar (FR-022).
# @RELATION CALLS -> [EXT:method:get_datasets_with_status]
@router.get("", response_model=DatasetsResponse)
async def get_datasets(
    env_id: str,
    search: str | None = None,
    filter: str | None = None,
    page: int = 1,
    page_size: int = 10,
    config_manager=Depends(get_config_manager),
    task_manager=Depends(get_task_manager),
    resource_service=Depends(get_resource_service),
    _ = Depends(has_permission("plugin:migration", "READ"))
):
    with belief_scope("get_datasets", f"env_id={env_id}, search={search}, filter={filter}, page={page}, page_size={page_size}"):
        # Validate pagination parameters
        if page < 1:
            logger.error(f"[get_datasets][Coherence:Failed] Invalid page: {page}")
            raise HTTPException(status_code=400, detail="Page must be >= 1")
        if page_size < 1 or page_size > 100:
            logger.error(f"[get_datasets][Coherence:Failed] Invalid page_size: {page_size}")
            raise HTTPException(status_code=400, detail="Page size must be between 1 and 100")

        # Validate environment exists
        environments = config_manager.get_environments()
        env = next((e for e in environments if e.id == env_id), None)
        if not env:
            logger.error(f"[get_datasets][Coherence:Failed] Environment not found: {env_id}")
            raise HTTPException(status_code=404, detail="Environment not found")

        try:
            # Get all tasks for status lookup
            all_tasks = task_manager.get_all_tasks()

            # Fetch datasets with status using ResourceService
            datasets = await resource_service.get_datasets_with_status(env, all_tasks)

            # Compute stats from full list BEFORE filtering/pagination
            total_datasets = len(datasets)
            unmapped = sum(
                1 for d in datasets
                if d.get("mapped_fields") and d["mapped_fields"].get("mapped", 0) == 0
            )
            mapped = sum(
                1 for d in datasets
                if d.get("mapped_fields") and d["mapped_fields"].get("mapped", 0) > 0
            )
            linked = sum(
                1 for d in datasets
                if d.get("linked_dashboard_count", 0) > 0
            )
            stats = StatsCounts(
                total=total_datasets,
                unmapped_count=unmapped,
                mapped_count=mapped,
                linked_count=linked,
            )

            # Apply server-side filter (before pagination) — FR-026
            if filter and filter in ("unmapped", "mapped", "linked"):
                if filter == "unmapped":
                    datasets = [
                        d for d in datasets
                        if d.get("mapped_fields") and d["mapped_fields"].get("mapped", 0) == 0
                    ]
                elif filter == "mapped":
                    datasets = [
                        d for d in datasets
                        if d.get("mapped_fields") and d["mapped_fields"].get("mapped", 0) > 0
                    ]
                elif filter == "linked":
                    datasets = [
                        d for d in datasets
                        if d.get("linked_dashboard_count", 0) > 0
                    ]

            # Apply search filter if provided
            if search:
                search_lower = search.lower()
                datasets = [
                    d for d in datasets
                    if search_lower in d.get('table_name', '').lower()
                ]

            # Calculate pagination
            total = len(datasets)
            total_pages = (total + page_size - 1) // page_size if total > 0 else 1
            start_idx = (page - 1) * page_size
            end_idx = start_idx + page_size

            # Slice datasets for current page
            paginated_datasets = datasets[start_idx:end_idx]

            logger.info(f"[get_datasets][Coherence:OK] Returning {len(paginated_datasets)} datasets (page {page}/{total_pages}, total: {total}, stats: total={stats.total} unmapped={stats.unmapped_count} mapped={stats.mapped_count} linked={stats.linked_count})")

            return DatasetsResponse(
                datasets=paginated_datasets,
                stats=stats,
                total=total,
                page=page,
                page_size=page_size,
                total_pages=total_pages
            )

        except Exception as e:
            logger.error(f"[get_datasets][Coherence:Failed] Failed to fetch datasets: {e}")
            raise HTTPException(status_code=503, detail=f"Failed to fetch datasets: {e!s}")
# #endregion get_datasets

# #region MapColumnsRequest [C:1] [TYPE DataClass]
# @BRIEF Request DTO for initiating column mapping
class MapColumnsRequest(BaseModel):
    env_id: str = Field(..., description="Environment ID")
    dataset_ids: list[int] = Field(..., description="List of dataset IDs to map")
    source_type: str = Field(..., description="Source type: 'sqllab' or 'xlsx'")
    database_id: int | None = Field(None, description="Superset database ID for SQL Lab source")
    sql_query: str | None = Field(None, description="Custom SQL query for SQL Lab (optional, defaults to information_schema.columns query)")
    file_data: str | None = Field(None, description="File path or data for XLSX source")
# #endregion MapColumnsRequest

# #region map_columns [C:4] [TYPE Function]
# @BRIEF Trigger bulk column mapping for datasets
# @PRE  User has permission plugin:mapper:execute
# @PRE  env_id is a valid environment ID
# @PRE  dataset_ids is a non-empty list
# @POST  Returns task_id for tracking mapping progress
# @POST  Task is created and queued for execution
# @RELATION DISPATCHES -> [MapperPlugin]
# @RELATION CALLS -> [create_task]
@router.post("/map-columns", response_model=TaskResponse)
async def map_columns(
    request: MapColumnsRequest,
    config_manager=Depends(get_config_manager),
    task_manager=Depends(get_task_manager),
    _ = Depends(has_permission("plugin:mapper", "EXECUTE"))
):
    with belief_scope("map_columns", f"env={request.env_id}, count={len(request.dataset_ids)}, source={request.source_type}"):
        # Validate request
        if not request.dataset_ids:
            logger.error("[map_columns][Coherence:Failed] No dataset IDs provided")
            raise HTTPException(status_code=400, detail="At least one dataset ID must be provided")

        # Validate source type
        if request.source_type not in ['sqllab', 'xlsx']:
            logger.error(f"[map_columns][Coherence:Failed] Invalid source type: {request.source_type}")
            raise HTTPException(status_code=400, detail="Source type must be 'sqllab' or 'xlsx'")

        # Validate database_id for sqllab source
        if request.source_type == 'sqllab' and not request.database_id:
            raise HTTPException(status_code=400, detail="database_id is required for 'sqllab' source type")

        # Validate environment exists
        environments = config_manager.get_environments()
        env = next((e for e in environments if e.id == request.env_id), None)

        if not env:
            logger.error(f"[map_columns][Coherence:Failed] Environment not found: {request.env_id}")
            raise HTTPException(status_code=404, detail="Environment not found")

        try:
            # Create mapping task
            task_params = {
                'env': request.env_id,
                'dataset_id': request.dataset_ids[0] if request.dataset_ids else None,
                'source': 'sqllab' if request.source_type == 'sqllab' else 'excel',
                'database_id': request.database_id,
                'sql_query': request.sql_query,
                'file_data': request.file_data
            }

            task_obj = await task_manager.create_task(
                plugin_id='dataset-mapper',
                params=task_params
            )

            logger.info(f"[map_columns][Coherence:OK] Mapping task created: {task_obj.id} for {len(request.dataset_ids)} datasets")

            return TaskResponse(task_id=str(task_obj.id))

        except Exception as e:
            logger.error(f"[map_columns][Coherence:Failed] Failed to create mapping task: {e}")
            raise HTTPException(status_code=503, detail=f"Failed to create mapping task: {e!s}")
# #endregion map_columns

# #region GenerateDocsRequest [C:1] [TYPE DataClass]
# @BRIEF Request DTO for initiating documentation generation
class GenerateDocsRequest(BaseModel):
    env_id: str = Field(..., description="Environment ID")
    dataset_ids: list[int] = Field(..., description="List of dataset IDs to generate docs for")
    llm_provider: str = Field(..., description="LLM provider to use")
    options: dict | None = Field(None, description="Additional options for documentation generation")
# #endregion GenerateDocsRequest

# #region generate_docs [C:4] [TYPE Function]
# @BRIEF Trigger bulk documentation generation for datasets
# @PRE  User has permission plugin:llm_analysis:execute
# @PRE  env_id is a valid environment ID
# @PRE  dataset_ids is a non-empty list
# @POST  Returns task_id for tracking documentation generation progress
# @POST  Task is created and queued for execution
# @RELATION DISPATCHES -> [DocumentationPlugin]
# @RELATION CALLS -> [create_task]
@router.post("/generate-docs", response_model=TaskResponse)
async def generate_docs(
    request: GenerateDocsRequest,
    config_manager=Depends(get_config_manager),
    task_manager=Depends(get_task_manager),
    _ = Depends(has_permission("plugin:llm_analysis", "EXECUTE"))
):
    with belief_scope("generate_docs", f"env={request.env_id}, count={len(request.dataset_ids)}, provider={request.llm_provider}"):
        # Validate request
        if not request.dataset_ids:
            logger.error("[generate_docs][Coherence:Failed] No dataset IDs provided")
            raise HTTPException(status_code=400, detail="At least one dataset ID must be provided")

        # Validate environment exists
        environments = config_manager.get_environments()
        env = next((e for e in environments if e.id == request.env_id), None)

        if not env:
            logger.error(f"[generate_docs][Coherence:Failed] Environment not found: {request.env_id}")
            raise HTTPException(status_code=404, detail="Environment not found")

        try:
            # Create documentation generation task
            task_params = {
                'environment_id': request.env_id,
                'dataset_id': str(request.dataset_ids[0]) if request.dataset_ids else None,
                'provider_id': request.llm_provider,
                'options': request.options or {}
            }

            task_obj = await task_manager.create_task(
                plugin_id='llm_documentation',
                params=task_params
            )

            logger.info(f"[generate_docs][Coherence:OK] Documentation generation task created: {task_obj.id} for {len(request.dataset_ids)} datasets")

            return TaskResponse(task_id=str(task_obj.id))

        except Exception as e:
            logger.error(f"[generate_docs][Coherence:Failed] Failed to create documentation generation task: {e}")
            raise HTTPException(status_code=503, detail=f"Failed to create documentation generation task: {e!s}")
# #endregion generate_docs

# #region get_dataset_detail [C:4] [TYPE Function]
# @BRIEF Get detailed dataset information including columns and linked dashboards
# @PRE  env_id is a valid environment ID
# @PRE  dataset_id is a valid dataset ID
# @POST  Returns detailed dataset info with columns and linked dashboards
# @RELATION CALLS -> [SupersetClientGetDatasetDetail]
@router.get("/{dataset_id}", response_model=DatasetDetailResponse)
async def get_dataset_detail(
    env_id: str,
    dataset_id: int,
    config_manager=Depends(get_config_manager),
    _ = Depends(has_permission("plugin:migration", "READ"))
):
    with belief_scope("get_dataset_detail", f"env_id={env_id}, dataset_id={dataset_id}"):
        # Validate environment exists
        environments = config_manager.get_environments()
        env = next((e for e in environments if e.id == env_id), None)
        if not env:
            logger.error(f"[get_dataset_detail][Coherence:Failed] Environment not found: {env_id}")
            raise HTTPException(status_code=404, detail="Environment not found")

        try:
            # Fetch detailed dataset info using SupersetClient
            client = SupersetClient(env)
            dataset_detail = client.get_dataset_detail(dataset_id)

            # Normalize 'database' field: Superset returns an object, model expects a string
            if isinstance(dataset_detail.get("database"), dict):
                db_obj = dataset_detail["database"]
                dataset_detail["database"] = db_obj.get("database_name", str(db_obj.get("id", "")))

            logger.info(f"[get_dataset_detail][Coherence:OK] Retrieved dataset {dataset_id} with {dataset_detail['column_count']} columns and {dataset_detail['linked_dashboard_count']} linked dashboards")

            return DatasetDetailResponse(**dataset_detail)

        except Exception as e:
            logger.error(f"[get_dataset_detail][Coherence:Failed] Failed to fetch dataset detail: {e}")
            raise HTTPException(status_code=503, detail=f"Failed to fetch dataset detail: {e!s}")
# #endregion get_dataset_detail

# #region _strip_html_tags [C:2] [TYPE Function] [SEMANTICS helper,validation,html]
# @BRIEF Detect and strip HTML tags from a text string. Uses simple regex <[^>]*>.
#        If HTML is detected, strips tags and logs a warning. Returns cleaned text.
# @REJECTED Reject-with-400 approach was rejected in favour of silent stripping — more user-friendly
#           and matches the UX expectation that inline-edited descriptions are plain text only.
def _strip_html_tags(text: str) -> str:
    if not text:
        return text
    if re.search(r"<[^>]*>", text):
        cleaned = re.sub(r"<[^>]*>", "", text)
        logger.warning("[_strip_html_tags] HTML tags detected and stripped from description")
        return cleaned
    return text
# #endregion _strip_html_tags

# #region update_column_description [C:4] [TYPE Endpoint]
# @BRIEF Save description for a single dataset column. Internal: GET full dataset from Superset, modify one column's description, PUT back.
# @PRE  dataset_id and column_id must exist in the target environment.
# @POST  Column description in Superset is updated. Response confirms success.
# @SIDE_EFFECT  Mutates dataset metadata in upstream Superset instance via PUT.
# @VALIDATION: description — string, max 2000 chars, plain text only (HTML stripped), may be empty string to clear description, no trimming applied. 404 if dataset or column not found. 502 if Superset upstream fails.
# @ERROR  400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or column_id not found. 502 — Superset upstream failure (GET or PUT).
# @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]
# @RELATION CALLS -> [EXT:method:SupersetClient.update_dataset]
# @RATIONALE Must perform GET→modify→PUT because Superset has no PATCH for individual columns — only full object PUT with override_columns=false.
# @REJECTED Direct PUT from frontend — rejected because frontend would need to handle full Superset payload structure.
@router.put("/{dataset_id}/columns/{column_id}/description")
async def update_column_description(
    dataset_id: int,
    column_id: int,
    env_id: str = Query(..., description="Environment ID"),
    body: ColumnDescriptionUpdate = ...,
    config_manager=Depends(get_config_manager),
    _ = Depends(has_permission("plugin:migration", "WRITE"))
):
    with belief_scope("update_column_description", f"dataset_id={dataset_id}, column_id={column_id}, env_id={env_id}"):
        # Validate and sanitize description
        description = _strip_html_tags(body.description)
        if len(description) > 2000:
            logger.error(f"[update_column_description][Coherence:Failed] Description exceeds 2000 chars: {len(description)}")
            raise HTTPException(status_code=400, detail="Description must not exceed 2000 characters")

        # Validate environment
        environments = config_manager.get_environments()
        env = next((e for e in environments if e.id == env_id), None)
        if not env:
            logger.error(f"[update_column_description][Coherence:Failed] Environment not found: {env_id}")
            raise HTTPException(status_code=404, detail="Environment not found")

        try:
            client = SupersetClient(env)

            # GET full dataset
            logger.reason("Fetching full dataset from Superset for column description update",
                          extra={"src": "update_column_description"})
            try:
                response = client.get_dataset(dataset_id)
            except Exception as e:
                logger.error(f"[update_column_description][Coherence:Failed] Superset GET failed: {e}")
                raise HTTPException(status_code=502, detail=f"Failed to fetch dataset from Superset: {e!s}")

            if isinstance(response, dict) and "result" in response:
                dataset = response["result"]
            else:
                dataset = response

            if not dataset or not dataset.get("id"):
                logger.error(f"[update_column_description][Coherence:Failed] Dataset {dataset_id} not found")
                raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")

            # Find and modify the target column
            columns = dataset.get("columns", [])
            found = False
            for col in columns:
                if col.get("id") == column_id:
                    col["description"] = description
                    found = True
                    break

            if not found:
                logger.error(f"[update_column_description][Coherence:Failed] Column {column_id} not found in dataset {dataset_id}")
                raise HTTPException(status_code=404, detail=f"Column {column_id} not found in dataset {dataset_id}")

            # PUT back with override_columns=false
            put_payload = dataset
            try:
                client.update_dataset(dataset_id, put_payload, override_columns=False)
            except Exception as e:
                logger.error(f"[update_column_description][Coherence:Failed] Superset PUT failed: {e}")
                raise HTTPException(status_code=502, detail=f"Failed to update dataset in Superset: {e!s}")

            logger.reflect(
                f"Updated column {column_id} description for dataset {dataset_id}",
                extra={"src": "update_column_description"},
            )
            return {"success": True, "description": description}

        except HTTPException:
            raise
        except Exception as e:
            logger.error(f"[update_column_description][Coherence:Failed] Unexpected error: {e}")
            raise HTTPException(status_code=500, detail=f"Unexpected error: {e!s}")
# #endregion update_column_description

# #region update_metric_description [C:4] [TYPE Endpoint]
# @BRIEF Save description for a single dataset metric. Mirror of update_column_description for metrics.
# @PRE  dataset_id and metric_id must exist in the target environment.
# @POST  Metric description in Superset is updated.
# @SIDE_EFFECT  Mutates dataset metadata in upstream Superset instance via PUT.
# @VALIDATION: description — string, max 2000 chars, plain text only (HTML stripped), may be empty string to clear description, no trimming applied. 404 if dataset or metric not found. 502 if Superset upstream fails.
# @ERROR  400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or metric_id not found. 502 — Superset upstream failure (GET or PUT).
# @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]
# @RELATION CALLS -> [EXT:method:SupersetClient.update_dataset]
@router.put("/{dataset_id}/metrics/{metric_id}/description")
async def update_metric_description(
    dataset_id: int,
    metric_id: int,
    env_id: str = Query(..., description="Environment ID"),
    body: MetricDescriptionUpdate = ...,
    config_manager=Depends(get_config_manager),
    _ = Depends(has_permission("plugin:migration", "WRITE"))
):
    with belief_scope("update_metric_description", f"dataset_id={dataset_id}, metric_id={metric_id}, env_id={env_id}"):
        # Validate and sanitize description
        description = _strip_html_tags(body.description)
        if len(description) > 2000:
            logger.error(f"[update_metric_description][Coherence:Failed] Description exceeds 2000 chars: {len(description)}")
            raise HTTPException(status_code=400, detail="Description must not exceed 2000 characters")

        # Validate environment
        environments = config_manager.get_environments()
        env = next((e for e in environments if e.id == env_id), None)
        if not env:
            logger.error(f"[update_metric_description][Coherence:Failed] Environment not found: {env_id}")
            raise HTTPException(status_code=404, detail="Environment not found")

        try:
            client = SupersetClient(env)

            # GET full dataset
            logger.reason("Fetching full dataset from Superset for metric description update",
                          extra={"src": "update_metric_description"})
            try:
                response = client.get_dataset(dataset_id)
            except Exception as e:
                logger.error(f"[update_metric_description][Coherence:Failed] Superset GET failed: {e}")
                raise HTTPException(status_code=502, detail=f"Failed to fetch dataset from Superset: {e!s}")

            if isinstance(response, dict) and "result" in response:
                dataset = response["result"]
            else:
                dataset = response

            if not dataset or not dataset.get("id"):
                logger.error(f"[update_metric_description][Coherence:Failed] Dataset {dataset_id} not found")
                raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")

            # Find and modify the target metric
            metrics = dataset.get("metrics", [])
            found = False
            for metric in metrics:
                if metric.get("id") == metric_id:
                    metric["description"] = description
                    found = True
                    break

            if not found:
                logger.error(f"[update_metric_description][Coherence:Failed] Metric {metric_id} not found in dataset {dataset_id}")
                raise HTTPException(status_code=404, detail=f"Metric {metric_id} not found in dataset {dataset_id}")

            # PUT back with override_columns=false
            try:
                client.update_dataset(dataset_id, dataset, override_columns=False)
            except Exception as e:
                logger.error(f"[update_metric_description][Coherence:Failed] Superset PUT failed: {e}")
                raise HTTPException(status_code=502, detail=f"Failed to update dataset in Superset: {e!s}")

            logger.reflect(
                f"Updated metric {metric_id} description for dataset {dataset_id}",
                extra={"src": "update_metric_description"},
            )
            return {"success": True, "description": description}

        except HTTPException:
            raise
        except Exception as e:
            logger.error(f"[update_metric_description][Coherence:Failed] Unexpected error: {e}")
            raise HTTPException(status_code=500, detail=f"Unexpected error: {e!s}")
# #endregion update_metric_description

# #endregion DatasetsApi
