# coding: utf-8

"""
    Mixpeek API

    This is the Mixpeek API, providing access to various endpoints for data processing and retrieval.

    The version of the OpenAPI document: 1.3.0
    Contact: info@mixpeek.com
    Generated by OpenAPI Generator (https://openapi-generator.tech)

    Do not edit the class manually.
"""  # noqa: E501


from __future__ import annotations
import pprint
import re  # noqa: F401
import json

from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator
from typing import Any, ClassVar, Dict, List, Optional
from typing_extensions import Annotated
from mixpeek.models.cluster_execution_centroid import ClusterExecutionCentroid
from mixpeek.models.cluster_execution_metrics import ClusterExecutionMetrics
from typing import Optional, Set
from typing_extensions import Self

class ClusterExecutionResult(BaseModel):
    """
    Complete results from a single clustering execution.  Represents the outcome of running a clustering algorithm on a collection's documents. Each execution creates a snapshot of clustering results at a point in time, including the clusters found, quality metrics, and semantic labels.  Use Cases:     - Display clustering execution history in UI     - Compare clustering quality across multiple runs     - Track execution status for long-running jobs     - Debug failed clustering attempts     - View cluster summaries and labels for analysis  Workflow:     1. Create cluster configuration → POST /clusters     2. Execute clustering → POST /clusters/{id}/execute     3. Poll execution status → GET /clusters/{id}/executions     4. View execution history → POST /clusters/{id}/executions/list  Status Lifecycle:     pending → processing → completed (or failed)  Note:     Execution results are immutable once completed. Re-running clustering     creates a new execution result with a new run_id.
    """ # noqa: E501
    run_id: Annotated[str, Field(strict=True)] = Field(description="REQUIRED. Unique identifier for this specific clustering execution. Format: 'run_' prefix followed by random alphanumeric string. Used to retrieve specific execution artifacts and results. Each re-execution of the same cluster creates a new run_id. References execution artifacts in S3 and MongoDB.")
    cluster_id: Annotated[str, Field(strict=True)] = Field(description="REQUIRED. Parent cluster configuration that was executed. Format: 'clust_' prefix followed by random alphanumeric string. Links this execution back to the cluster definition. Multiple executions can share the same cluster_id.")
    status: StrictStr = Field(description="REQUIRED. Current status of the clustering execution. Values:   'pending' = Job queued, waiting to start.   'processing' = Clustering algorithm running (may take minutes for large datasets).   'completed' = Clustering finished successfully, results available.   'failed' = Clustering failed, check error_message for details. Status changes: pending → processing → (completed OR failed). Poll this field to track job progress.")
    num_clusters: Annotated[int, Field(strict=True, ge=0)] = Field(description="REQUIRED. Number of clusters found by the clustering algorithm. Range: 1 to num_points (though typically much lower). Interpretation:   Too few clusters = overgeneralization, may need lower n_clusters param.   Too many clusters = overfitting, may need higher n_clusters param.   Optimal value depends on dataset and use case. Available immediately upon completion, even if metrics fail.")
    num_points: Annotated[int, Field(strict=True, ge=0)] = Field(description="REQUIRED. Total number of documents/points that were clustered. Equals the count of documents in the collection at execution time. Note: This may differ across executions if documents were added/removed. Used to calculate metrics and validate clustering quality. Minimum 2 points required for clustering (1 cluster per point otherwise).")
    metrics: Optional[ClusterExecutionMetrics] = Field(default=None, description="OPTIONAL. Quality metrics evaluating clustering performance. NOT REQUIRED - only present for successful executions. null if:   - Execution is still pending/processing.   - Execution failed.   - Too few points to calculate metrics (need 2+ points). Contains silhouette_score, davies_bouldin_index, calinski_harabasz_score. Use to compare quality across multiple executions.")
    centroids: Optional[List[ClusterExecutionCentroid]] = Field(default=None, description="OPTIONAL. List of cluster centroids with semantic labels. NOT REQUIRED - only present for completed executions with LLM labeling enabled. Length: equals num_clusters. Each centroid contains:   - cluster_id: Identifier for the cluster (e.g., 'cl_0').   - num_members: Count of documents in this cluster.   - label: Human-readable cluster name (e.g., 'Product Reviews').   - summary: Brief description of cluster content.   - keywords: Array of representative terms. null if:   - Execution pending/processing/failed.   - LLM labeling not configured. Use for: Displaying cluster summaries in UI, filtering by cluster.")
    created_at: datetime = Field(description="REQUIRED. Timestamp when the clustering execution started. ISO 8601 format with timezone (UTC). Used to:   - Sort executions chronologically.   - Calculate execution duration (completed_at - created_at).   - Filter execution history by date range. Always present, even for failed executions.")
    completed_at: Optional[datetime] = Field(default=None, description="OPTIONAL. Timestamp when the clustering execution finished. ISO 8601 format with timezone (UTC). NOT REQUIRED - only present for completed or failed executions. null if: status is 'pending' or 'processing'. Use to:   - Calculate execution duration (completed_at - created_at).   - Show when results became available. Present for both successful and failed executions.")
    error_message: Optional[StrictStr] = Field(default=None, description="OPTIONAL. Error message if the clustering execution failed. NOT REQUIRED - only present when status is 'failed'. null if: execution succeeded or is still in progress. Contains:   - Human-readable error description.   - Possible causes and suggested fixes.   - Stack trace details (for debugging). Common errors:   - 'Insufficient documents for clustering' (need 2+ docs).   - 'Feature extractor not found' (invalid collection config).   - 'Out of memory' (dataset too large for algorithm). Use for: Debugging failed executions and user error messages.")
    llm_labeling_errors: Optional[List[StrictStr]] = Field(default=None, description="OPTIONAL. List of errors encountered during LLM labeling. NOT REQUIRED - only present when LLM labeling was attempted and encountered errors. null if:   - LLM labeling was not enabled.   - LLM labeling succeeded for all clusters.   - Execution is still in progress. Each error is a JSON string containing:   - 'error': Human-readable error message.   - 'clusters': List of cluster IDs affected by this error. Common errors:   - 'LLM API timeout for 2 clusters' (network/API issues).   - 'OpenAI rate limit exceeded' (quota exhausted).   - 'Invalid model name: gpt-3.5' (config error).   - 'No representative documents for cluster cl_3' (empty cluster). Use for:   - Debugging why some clusters have fallback labels.   - Identifying LLM API issues without failing entire clustering.   - Warning users about partial labeling success.")
    __properties: ClassVar[List[str]] = ["run_id", "cluster_id", "status", "num_clusters", "num_points", "metrics", "centroids", "created_at", "completed_at", "error_message", "llm_labeling_errors"]

    @field_validator('run_id')
    def run_id_validate_regular_expression(cls, value):
        """Validates the regular expression"""
        if not re.match(r"^run_[a-zA-Z0-9]+$", value):
            raise ValueError(r"must validate the regular expression /^run_[a-zA-Z0-9]+$/")
        return value

    @field_validator('cluster_id')
    def cluster_id_validate_regular_expression(cls, value):
        """Validates the regular expression"""
        if not re.match(r"^clust_[a-zA-Z0-9]+$", value):
            raise ValueError(r"must validate the regular expression /^clust_[a-zA-Z0-9]+$/")
        return value

    @field_validator('status')
    def status_validate_enum(cls, value):
        """Validates the enum"""
        if value not in set(['pending', 'processing', 'completed', 'failed']):
            raise ValueError("must be one of enum values ('pending', 'processing', 'completed', 'failed')")
        return value

    model_config = ConfigDict(
        populate_by_name=True,
        validate_assignment=True,
        protected_namespaces=(),
    )


    def to_str(self) -> str:
        """Returns the string representation of the model using alias"""
        return pprint.pformat(self.model_dump(by_alias=True))

    def to_json(self) -> str:
        """Returns the JSON representation of the model using alias"""
        # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
        return json.dumps(self.to_dict())

    @classmethod
    def from_json(cls, json_str: str) -> Optional[Self]:
        """Create an instance of ClusterExecutionResult from a JSON string"""
        return cls.from_dict(json.loads(json_str))

    def to_dict(self) -> Dict[str, Any]:
        """Return the dictionary representation of the model using alias.

        This has the following differences from calling pydantic's
        `self.model_dump(by_alias=True)`:

        * `None` is only added to the output dict for nullable fields that
          were set at model initialization. Other fields with value `None`
          are ignored.
        """
        excluded_fields: Set[str] = set([
        ])

        _dict = self.model_dump(
            by_alias=True,
            exclude=excluded_fields,
            exclude_none=True,
        )
        # override the default output from pydantic by calling `to_dict()` of metrics
        if self.metrics:
            _dict['metrics'] = self.metrics.to_dict()
        # override the default output from pydantic by calling `to_dict()` of each item in centroids (list)
        _items = []
        if self.centroids:
            for _item_centroids in self.centroids:
                if _item_centroids:
                    _items.append(_item_centroids.to_dict())
            _dict['centroids'] = _items
        return _dict

    @classmethod
    def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
        """Create an instance of ClusterExecutionResult from a dict"""
        if obj is None:
            return None

        if not isinstance(obj, dict):
            return cls.model_validate(obj)

        _obj = cls.model_validate({
            "run_id": obj.get("run_id"),
            "cluster_id": obj.get("cluster_id"),
            "status": obj.get("status"),
            "num_clusters": obj.get("num_clusters"),
            "num_points": obj.get("num_points"),
            "metrics": ClusterExecutionMetrics.from_dict(obj["metrics"]) if obj.get("metrics") is not None else None,
            "centroids": [ClusterExecutionCentroid.from_dict(_item) for _item in obj["centroids"]] if obj.get("centroids") is not None else None,
            "created_at": obj.get("created_at"),
            "completed_at": obj.get("completed_at"),
            "error_message": obj.get("error_message"),
            "llm_labeling_errors": obj.get("llm_labeling_errors")
        })
        return _obj


