#!/usr/bin/env python

"""████
 ██    ██    Datature
   ██  ██    Powering Breakthrough AI
     ██

@File    :   training_projects.py
@Author  :   Wei Loon Cheng
@Contact :   developers@datature.io
@License :   Apache License 2.0
@Desc    :   Datature Vi SDK training projects module.
"""

import uuid
from typing import Any

import msgspec
from vi.api.pagination import PaginatedResponse
from vi.api.resources.training_projects import responses
from vi.api.resources.training_projects.links import TrainingProjectLinkParser
from vi.api.resources.training_projects.types import TrainingProjectListParams
from vi.api.responses import DeletedResource, Pagination
from vi.api.types import PaginationParams
from vi.client.auth import Authentication
from vi.client.http.requester import Requester
from vi.client.rest.resource import RESTResource
from vi.client.validation import validate_id_param, validate_pagination_params


class TrainingProject(RESTResource):
    """Training project resource for managing training projects.

    This class provides methods to list, retrieve, create, update, and delete
    training projects. Training projects organize related flows and runs into
    logical groups for tracking training experiments.

    Training projects also support aggregation insights for comparing run
    performance across a project, and dry run validation for checking
    project creation before committing.

    Example:
        ```python
        import vi

        client = vi.Client()

        # List all training projects
        projects = client.training_projects.list()
        for project in projects.items:
            print(f"Project: {project.training_project_id} - {project.spec.name}")

        # Get a specific training project
        project = client.training_projects.get(training_project_id="project_abc123")
        project.info()

        # Create a new training project
        project = client.training_projects.create(
            spec={"name": "My Project", "type": "vqa"}
        )

        # Access flows and runs through the client
        flows = client.flows.list()
        runs = client.runs.list()
        ```

    See Also:
        - [`Flow`](../../api/resources/flows.md): Training flow resource
        - [`Run`](../../api/resources/runs.md): Training run resource

    """

    _link_parser: TrainingProjectLinkParser

    def __init__(self, auth: Authentication, requester: Requester):
        """Initialize the TrainingProject resource.

        Args:
            auth: Authentication instance containing credentials.
            requester: HTTP requester instance for making API calls.

        """
        super().__init__(auth, requester)
        self._link_parser = TrainingProjectLinkParser(auth.organization_id)

    def list(
        self, pagination: PaginationParams | dict = PaginationParams()
    ) -> PaginatedResponse[responses.TrainingProject]:
        """List all training projects in the organization.

        Retrieves a paginated list of all training projects accessible to the
        authenticated user within their organization.

        Args:
            pagination: Pagination parameters for controlling page size and offsets.
                Can be a PaginationParams object or dict. Defaults to first page
                with default page size.

        Returns:
            PaginatedResponse containing TrainingProject objects with navigation support.

        Raises:
            ViOperationError: If the API returns an unexpected response format.
            ViValidationError: If pagination parameters are invalid.
            ViAuthenticationError: If authentication fails.

        Example:
            ```python
            # List all training projects
            projects = client.training_projects.list()
            for project in projects.items:
                print(f"{project.training_project_id}: {project.spec.name}")

            # With pagination
            projects = client.training_projects.list(pagination={"page_size": 10})
            ```

        """
        if isinstance(pagination, dict):
            validate_pagination_params(**pagination)
            pagination = PaginationParams(**pagination)

        params = TrainingProjectListParams(pagination=pagination)

        response = self._requester.get(
            self._link_parser(),
            params=params.to_query_params(),
            response_type=Pagination[responses.TrainingProject],
        )

        if isinstance(response, Pagination):
            return PaginatedResponse(
                items=response.items,
                next_page=response.next_page,
                prev_page=response.prev_page,
                list_method=self.list,
                method_kwargs={"pagination": pagination},
            )

        raise ValueError(f"Invalid response {response} with type {type(response)}")

    def get(self, training_project_id: str) -> responses.TrainingProject:
        """Get detailed information about a specific training project.

        Args:
            training_project_id: The unique identifier of the training project.

        Returns:
            TrainingProject object containing all project information.

        Raises:
            ViNotFoundError: If the training project doesn't exist.
            ViValidationError: If the training_project_id format is invalid.
            ViOperationError: If the API returns an unexpected response format.

        Example:
            ```python
            project = client.training_projects.get(training_project_id="project_abc123")
            print(f"Name: {project.spec.name}")
            print(f"Type: {project.spec.type}")
            project.info()
            ```

        """
        validate_id_param(training_project_id, "training_project_id")

        response = self._requester.get(
            self._link_parser(training_project_id),
            response_type=responses.TrainingProject,
        )

        if isinstance(response, responses.TrainingProject):
            return response

        raise ValueError(f"Invalid response {response} with type {type(response)}")

    def create(
        self,
        spec: responses.TrainingProjectSpec | dict,
        training_project_id: str | None = None,
        metadata: dict[str, str] | None = None,
    ) -> responses.TrainingProject:
        """Create a new training project.

        Creates a new training project with the specified configuration using PUT.
        A unique project ID is automatically generated if not provided.

        Args:
            spec: Training project specification including name and type.
                Can be a TrainingProjectSpec object or dict.
            training_project_id: Optional custom project ID. If None, a UUID
                is auto-generated.
            metadata: Optional metadata attributes as key-value pairs.

        Returns:
            TrainingProject object with the created project information.

        Raises:
            ViValidationError: If spec is malformed or missing required fields.
            ViOperationError: If creation fails or API returns unexpected response.

        Example:
            ```python
            # Create with dict spec
            project = client.training_projects.create(
                spec={"name": "My Project", "type": "vqa"}
            )
            print(f"Created: {project.training_project_id}")

            # Create with custom ID
            project = client.training_projects.create(
                spec={"name": "My Project", "type": "phrase-grounding"},
                training_project_id="my-custom-id",
            )

            # Create with metadata
            project = client.training_projects.create(
                spec={"name": "My Project", "type": "freeform"},
                metadata={"environment": "production"},
            )
            ```

        """
        if training_project_id is None:
            training_project_id = str(uuid.uuid4())

        if isinstance(spec, responses.TrainingProjectSpec):
            spec_dict = msgspec.to_builtins(spec)
        else:
            spec_dict = spec

        request_body: dict[str, Any] = {
            "kind": "TrainingProject",
            "trainingProjectId": training_project_id,
            "spec": spec_dict,
        }

        if metadata is not None:
            request_body["metadata"] = {"attributes": metadata}

        response = self._requester.put(
            self._link_parser(training_project_id),
            json_data=request_body,
            response_type=responses.TrainingProject,
        )

        if isinstance(response, responses.TrainingProject):
            return response

        raise ValueError(f"Invalid response {response} with type {type(response)}")

    def update(
        self,
        training_project_id: str,
        spec: dict | None = None,
        metadata: dict[str, str | None] | None = None,
    ) -> responses.TrainingProject:
        """Update an existing training project.

        Updates specific fields of a training project using PATCH semantics.
        Only provided fields are updated; omitted fields remain unchanged.

        Note: The project type cannot be changed after creation.

        Args:
            training_project_id: Unique identifier of the project to update.
            spec: Partial spec with fields to update. Supports "name" and
                "description" fields. The "type" field cannot be changed.
            metadata: Optional metadata attributes to update. Set a value to
                None to remove that attribute.

        Returns:
            TrainingProject object with updated information.

        Raises:
            ViNotFoundError: If project doesn't exist.
            ViValidationError: If training_project_id or spec is invalid.
            ViOperationError: If update fails or API returns unexpected response.

        Example:
            ```python
            # Update name
            project = client.training_projects.update(
                training_project_id="project_abc123",
                spec={"name": "Updated Name"},
            )

            # Update description
            project = client.training_projects.update(
                training_project_id="project_abc123",
                spec={"description": "New description"},
            )

            # Update metadata
            project = client.training_projects.update(
                training_project_id="project_abc123",
                metadata={"environment": "staging"},
            )
            ```

        """
        validate_id_param(training_project_id, "training_project_id")

        request_body: dict[str, Any] = {}

        if spec is not None:
            request_body["spec"] = spec

        if metadata is not None:
            request_body["metadata"] = {"attributes": metadata}

        response = self._requester.patch(
            self._link_parser(training_project_id),
            json_data=request_body,
            response_type=responses.TrainingProject,
        )

        if isinstance(response, responses.TrainingProject):
            return response

        raise ValueError(f"Invalid response {response} with type {type(response)}")

    def delete(self, training_project_id: str) -> DeletedResource:
        """Delete a training project by ID.

        Permanently removes a training project. This operation cannot be undone.

        Args:
            training_project_id: The unique identifier of the project to delete.

        Returns:
            DeletedResource object confirming the deletion.

        Raises:
            ViNotFoundError: If the training project doesn't exist.
            ViValidationError: If the training_project_id format is invalid.
            ViPermissionError: If user lacks permission to delete.
            ViOperationError: If the deletion fails.

        Example:
            ```python
            result = client.training_projects.delete(
                training_project_id="project_abc123"
            )
            print(f"Deleted: {result.id}")
            ```

        """
        validate_id_param(training_project_id, "training_project_id")

        response = self._requester.delete(self._link_parser(training_project_id))

        if isinstance(response, dict) and response.get("data") == "":
            return DeletedResource(id=training_project_id, deleted=True)

        raise ValueError(f"Invalid response {response} with type {type(response)}")

    def get_aggregation_insights(
        self, training_project_id: str
    ) -> responses.AggregationInsightsResult:
        """Get aggregation insights for a training project.

        Retrieves run aggregation insights including hyperparameters and metrics
        across all runs in the training project.

        Args:
            training_project_id: The unique identifier of the training project.

        Returns:
            AggregationInsightsResult containing aggregation data and/or
            pending operation status.

        Raises:
            ViNotFoundError: If the training project doesn't exist.
            ViValidationError: If the training_project_id format is invalid.

        Example:
            ```python
            insights = client.training_projects.get_aggregation_insights(
                training_project_id="project_abc123"
            )
            if insights.payload:
                for run_id, data in insights.payload.results.items():
                    print(f"Run {run_id}: {data}")
            ```

        """
        validate_id_param(training_project_id, "training_project_id")

        response = self._requester.get(
            self._link_parser.aggregation_insights(training_project_id),
            response_type=responses.AggregationInsightsResult,
        )

        if isinstance(response, responses.AggregationInsightsResult):
            return response

        raise ValueError(f"Invalid response {response} with type {type(response)}")

    def generate_aggregation_insights(self, training_project_id: str) -> None:
        """Generate aggregation insights for a training project.

        Triggers the generation of run aggregation insights for the training
        project. This is an asynchronous operation - use get_aggregation_insights()
        to check the status and retrieve results.

        Args:
            training_project_id: The unique identifier of the training project.

        Raises:
            ViNotFoundError: If the training project doesn't exist.
            ViValidationError: If the training_project_id format is invalid.

        Example:
            ```python
            # Trigger generation
            client.training_projects.generate_aggregation_insights(
                training_project_id="project_abc123"
            )

            # Check status later
            insights = client.training_projects.get_aggregation_insights(
                training_project_id="project_abc123"
            )
            if insights.pending_operation:
                print(f"Status: {insights.pending_operation.status}")
            ```

        """
        validate_id_param(training_project_id, "training_project_id")

        self._requester.post(
            self._link_parser.generate_aggregation_insights(training_project_id),
        )

    def dry_run_create(
        self,
        training_project_id: str,
        name: str,
    ) -> responses.DryRunResponse:
        """Dry run validation for creating a training project.

        Validates whether a training project can be created with the given
        ID and name without actually creating it. Checks for ID and name
        conflicts.

        Args:
            training_project_id: The desired project ID to validate.
            name: The desired project name to validate.

        Returns:
            DryRunResponse containing any validation errors (IdConflict,
            NameConflict).

        Example:
            ```python
            result = client.training_projects.dry_run_create(
                training_project_id="my-project",
                name="My Project",
            )
            if result.errors:
                for error in result.errors:
                    print(f"Conflict: {error.kind}")
            else:
                print("No conflicts - safe to create")
            ```

        """
        request_body: dict[str, Any] = {
            "trainingProjectId": training_project_id,
            "spec": {"name": name},
        }

        response = self._requester.post(
            self._link_parser.dry_run(),
            json_data=request_body,
            response_type=responses.DryRunResponse,
        )

        if isinstance(response, responses.DryRunResponse):
            return response

        raise ValueError(f"Invalid response {response} with type {type(response)}")

    def help(self) -> None:
        """Display helpful information about using the TrainingProject resource.

        Example:
            ```python
            client.training_projects.help()
            ```

        """
        help_text = """
Training Project Resource - Quick Help

  COMMON OPERATIONS:

  Create a new training project:
    project = client.training_projects.create(
        spec={"name": "My Project", "type": "vqa"}
    )

  List all training projects:
    projects = client.training_projects.list()
    for project in projects.items:
        print(f"{project.training_project_id}: {project.spec.name}")

  Get a specific training project:
    project = client.training_projects.get(
        training_project_id="project_abc123"
    )
    project.info()

  Update a training project:
    project = client.training_projects.update(
        training_project_id="project_abc123",
        spec={"name": "Updated Name"}
    )

  Delete a training project:
    result = client.training_projects.delete(
        training_project_id="project_abc123"
    )

  Validate before creating:
    result = client.training_projects.dry_run_create(
        training_project_id="my-project",
        name="My Project"
    )

  Get aggregation insights:
    insights = client.training_projects.get_aggregation_insights(
        training_project_id="project_abc123"
    )

  Generate aggregation insights:
    client.training_projects.generate_aggregation_insights(
        training_project_id="project_abc123"
    )

  AVAILABLE METHODS:

  * list(pagination=...)                         - List all training projects
  * get(training_project_id)                     - Get a specific project
  * create(spec, ...)                            - Create a new project (PUT)
  * update(training_project_id, ...)             - Update a project (PATCH)
  * delete(training_project_id)                  - Delete a project
  * get_aggregation_insights(training_project_id) - Get run insights
  * generate_aggregation_insights(...)           - Trigger insight generation
  * dry_run_create(training_project_id, name)    - Validate creation

  RELATED RESOURCES:

  * client.flows   - Manage training flows
  * client.runs    - Manage training runs

  PROJECT TYPES:

  * phrase-grounding  - Phrase grounding tasks
  * vqa               - Visual question answering
  * freeform          - Freeform training

Documentation: https://vi.developers.datature.com/docs/vi-sdk
"""
        print(help_text)
