"""Service for managing Launches in Allure TestOps."""

from collections.abc import Sequence
from dataclasses import dataclass
from typing import Literal

from pydantic import ValidationError as PydanticValidationError

from src.client import AllureClient, FindAll29200Response, LaunchCreateDto, LaunchDto, LaunchUploadResponseDto
from src.client.exceptions import AllureAPIError, AllureNotFoundError, AllureValidationError, LaunchNotFoundError
from src.client.generated.models.aql_validate_response_dto import AqlValidateResponseDto
from src.client.generated.models.external_link_dto import ExternalLinkDto
from src.client.generated.models.issue_dto import IssueDto
from src.client.generated.models.launch_existing_upload_dto import LaunchExistingUploadDto
from src.client.generated.models.launch_preview_dto import LaunchPreviewDto
from src.client.generated.models.launch_tag_dto import LaunchTagDto
from src.client.generated.models.page_launch_dto import PageLaunchDto
from src.client.generated.models.page_launch_preview_dto import PageLaunchPreviewDto
from src.utils.schema_hint import generate_schema_hint

MAX_NAME_LENGTH = 255
MAX_TAG_LENGTH = 255

type LaunchListItem = LaunchDto | LaunchPreviewDto


@dataclass
class LaunchListResult:
    """Result of listing launches."""

    items: Sequence[LaunchListItem]
    total: int
    page: int
    size: int
    total_pages: int


@dataclass
class LaunchDeleteResult:
    """Result of a launch delete operation."""

    launch_id: int
    status: Literal["archived", "already_deleted"]
    message: str
    name: str | None = None


class LaunchService:
    """Service for launch create/list operations."""

    def __init__(self, client: AllureClient) -> None:
        self._client = client
        self._project_id = client.get_project()

    async def create_launch(
        self,
        name: str,
        autoclose: bool | None = None,
        external: bool | None = None,
        issues: list[dict[str, object]] | None = None,
        links: list[dict[str, str]] | None = None,
        tags: list[str] | None = None,
    ) -> LaunchDto:
        """Create a new launch.

        Args:
            name: Launch name.
            autoclose: Whether the launch auto-closes.
            external: Whether the launch is external.
            issues: Optional list of issue dictionaries.
            links: Optional list of external link dictionaries.
            tags: Optional list of launch tags.

        Returns:
            The created launch.
        """
        self._validate_project_id(self._project_id)
        self._validate_name(name)
        self._validate_tags(tags)
        self._validate_links(links)
        self._validate_issues(issues)

        try:
            issue_dtos = self._build_issue_dtos(issues)
            link_dtos = self._build_link_dtos(links)
            tag_dtos = self._build_tag_dtos(tags)

            data = LaunchCreateDto(
                name=name,
                project_id=self._project_id,
                autoclose=autoclose,
                external=external,
                issues=issue_dtos,
                links=link_dtos,
                tags=tag_dtos,
            )
        except PydanticValidationError as e:
            hint = generate_schema_hint(LaunchCreateDto)
            raise AllureValidationError(f"Invalid launch data: {e}", suggestions=[hint]) from e

        return await self._client.create_launch(data)

    async def list_launches(
        self,
        page: int = 0,
        size: int = 20,
        search: str | None = None,
        filter_id: int | None = None,
        sort: list[str] | None = None,
    ) -> LaunchListResult:
        """List launches for the configured project.

        Args:
            page: Zero-based page index.
            size: Page size.
            search: Optional name search.
            filter_id: Optional filter ID.
            sort: Optional sort criteria.

        Returns:
            LaunchListResult with items and pagination metadata.
        """
        self._validate_project_id(self._project_id)
        self._validate_pagination(page, size)

        response = await self._client.list_launches(
            project_id=self._project_id,
            page=page,
            size=size,
            search=search,
            filter_id=filter_id,
            sort=sort,
        )

        page_data = self._extract_page(response)

        items = page_data.content or []

        return LaunchListResult(
            items=items,
            total=page_data.total_elements or len(items),
            page=page_data.number or page,
            size=page_data.size or size,
            total_pages=page_data.total_pages or 1,
        )

    async def search_launches_aql(
        self,
        rql: str,
        page: int = 0,
        size: int = 20,
        sort: list[str] | None = None,
    ) -> LaunchListResult:
        """Search launches using raw AQL.

        Args:
            rql: Raw AQL query string.
            page: Zero-based page index.
            size: Page size (max 100).
            sort: Optional sort criteria.

        Returns:
            LaunchListResult with items and pagination metadata.
        """
        self._validate_project_id(self._project_id)
        if not isinstance(rql, str) or not rql.strip():
            raise AllureValidationError("AQL query must be a non-empty string")
        self._validate_pagination(page, size)

        response = await self._client.search_launches_aql(
            project_id=self._project_id,
            rql=rql,
            page=page,
            size=size,
            sort=sort,
        )

        items = response.content or []

        return LaunchListResult(
            items=items,
            total=response.total_elements or len(items),
            page=response.number or page,
            size=response.size or size,
            total_pages=response.total_pages or 1,
        )

    async def get_launch(self, launch_id: int) -> LaunchDto:
        """Retrieve a specific launch by its ID.

        Args:
            launch_id: The unique ID of the launch.

        Returns:
            The launch data.
        """
        self._validate_project_id(self._project_id)
        self._validate_launch_id(launch_id)

        try:
            return await self._client.get_launch(launch_id)
        except AllureNotFoundError as exc:
            raise LaunchNotFoundError(
                launch_id=launch_id,
                status_code=exc.status_code,
                response_body=exc.response_body,
            ) from exc

    async def close_launch(self, launch_id: int) -> LaunchDto:
        """Close a launch and return updated launch details."""
        self._validate_project_id(self._project_id)
        self._validate_launch_id(launch_id)

        pre_close = await self.get_launch(launch_id)

        try:
            await self._client.close_launch(launch_id)
            closed_launch = await self._client.get_launch(launch_id)
        except AllureNotFoundError as exc:
            raise LaunchNotFoundError(
                launch_id=launch_id,
                status_code=exc.status_code,
                response_body=exc.response_body,
            ) from exc

        if closed_launch.closed is not True:
            raise AllureAPIError(
                f"Launch {launch_id} was not closed by API",
                suggestions=["Retry close_launch", "Inspect launch state via get_launch"],
            )

        close_report = self._determine_close_report_status(pre_close, closed_launch)
        closed_launch.__dict__["close_report_generation"] = close_report
        return closed_launch

    async def reopen_launch(self, launch_id: int) -> LaunchDto:
        """Reopen a launch and return updated launch details."""
        self._validate_project_id(self._project_id)
        self._validate_launch_id(launch_id)

        try:
            await self._client.reopen_launch(launch_id)
            reopened = await self._client.get_launch(launch_id)
        except AllureNotFoundError as exc:
            raise LaunchNotFoundError(
                launch_id=launch_id,
                status_code=exc.status_code,
                response_body=exc.response_body,
            ) from exc

        if reopened.closed is True:
            raise AllureAPIError(
                f"Launch {launch_id} was not reopened by API",
                suggestions=["Retry reopen_launch", "Inspect launch state via get_launch"],
            )

        return reopened

    async def upload_results_to_launch(
        self,
        launch_id: int,
        files: list[bytes | str | tuple[str, bytes]],
    ) -> LaunchUploadResponseDto:
        """Upload result files to an existing launch."""
        self._validate_project_id(self._project_id)
        self._validate_launch_id(launch_id)

        if not isinstance(files, list) or not files:
            raise AllureValidationError("files must be a non-empty list")

        upload_info = LaunchExistingUploadDto()
        return await self._client.upload_results_to_launch(launch_id=launch_id, files=files, info=upload_info)

    @staticmethod
    def _determine_close_report_status(pre_close: LaunchDto, closed_launch: LaunchDto) -> str:
        if pre_close.closed is True:
            return "already-closed"

        status = getattr(closed_launch, "status", None)
        if isinstance(status, str) and status.strip():
            return status

        return "scheduled"

    async def delete_launch(self, launch_id: int) -> LaunchDeleteResult:
        """Delete/archive a launch by ID with idempotent behavior."""
        self._validate_project_id(self._project_id)
        self._validate_launch_id(launch_id)

        try:
            launch = await self.get_launch(launch_id)
        except LaunchNotFoundError:
            return LaunchDeleteResult(
                launch_id=launch_id,
                status="already_deleted",
                message=f"Launch {launch_id} was already archived or doesn't exist.",
            )

        await self._client.delete_launch(launch_id)

        return LaunchDeleteResult(
            launch_id=launch_id,
            status="archived",
            name=launch.name,
            message=f"Launch {launch_id} has been archived.",
        )

    async def validate_launch_query(self, rql: str) -> tuple[bool, int | None]:
        """Validate an AQL query for launches."""
        if not isinstance(rql, str) or not rql.strip():
            raise AllureValidationError("AQL query must be a non-empty string")

        response = await self._client.validate_launch_query(project_id=self._project_id, rql=rql)
        if not isinstance(response, AqlValidateResponseDto):
            raise AllureValidationError("Unexpected validation response from API")
        return (response.valid or False, response.count)

    @staticmethod
    def _extract_page(response: FindAll29200Response) -> PageLaunchDto | PageLaunchPreviewDto:
        actual = response.actual_instance
        if isinstance(actual, (PageLaunchDto, PageLaunchPreviewDto)):
            return actual
        raise AllureValidationError("Unexpected launch list response from API")

    def _validate_project_id(self, project_id: int | None) -> None:
        if not isinstance(project_id, int):
            raise AllureValidationError(f"Project ID must be an integer, got {type(project_id).__name__}")
        if project_id <= 0:
            raise AllureValidationError("Project ID is required and must be positive")

    @staticmethod
    def _validate_pagination(page: int, size: int) -> None:
        if not isinstance(page, int) or page < 0:
            raise AllureValidationError("Page must be a non-negative integer")
        if not isinstance(size, int) or size <= 0 or size > 100:
            raise AllureValidationError("Size must be between 1 and 100")

    @staticmethod
    def _validate_launch_id(launch_id: int) -> None:
        if not isinstance(launch_id, int):
            raise AllureValidationError(f"Launch ID must be an integer, got {type(launch_id).__name__}")
        if launch_id <= 0:
            raise AllureValidationError("Launch ID must be a positive integer")

    @staticmethod
    def _validate_name(name: str) -> None:
        if not isinstance(name, str):
            raise AllureValidationError(f"Launch name must be a string, got {type(name).__name__}")
        if not name.strip():
            raise AllureValidationError("Launch name is required")
        if len(name) > MAX_NAME_LENGTH:
            raise AllureValidationError(f"Launch name must be {MAX_NAME_LENGTH} characters or less")

    @staticmethod
    def _validate_tags(tags: list[str] | None) -> None:
        if tags is None:
            return
        if not isinstance(tags, list):
            raise AllureValidationError(f"Tags must be a list, got {type(tags).__name__}")
        for i, tag in enumerate(tags):
            if not isinstance(tag, str):
                raise AllureValidationError(f"Tag at index {i} must be a string, got {type(tag).__name__}")
            if not tag.strip():
                raise AllureValidationError(f"Tag at index {i} cannot be empty")
            if len(tag) > MAX_TAG_LENGTH:
                raise AllureValidationError(f"Tag at index {i} must be {MAX_TAG_LENGTH} characters or less")

    @staticmethod
    def _validate_links(links: list[dict[str, str]] | None) -> None:
        if links is None:
            return
        if not isinstance(links, list):
            raise AllureValidationError(f"Links must be a list, got {type(links).__name__}")
        for i, link in enumerate(links):
            if not isinstance(link, dict):
                raise AllureValidationError(f"Link at index {i} must be a dictionary")
            if not link:
                raise AllureValidationError(f"Link at index {i} cannot be empty")
            url = link.get("url")
            if url is not None and not isinstance(url, str):
                raise AllureValidationError(f"Link at index {i} 'url' must be a string")
            name = link.get("name")
            if name is not None and not isinstance(name, str):
                raise AllureValidationError(f"Link at index {i} 'name' must be a string")
            link_type = link.get("type")
            if link_type is not None and not isinstance(link_type, str):
                raise AllureValidationError(f"Link at index {i} 'type' must be a string")

    @staticmethod
    def _validate_issues(issues: list[dict[str, object]] | None) -> None:
        if issues is None:
            return
        if not isinstance(issues, list):
            raise AllureValidationError(f"Issues must be a list, got {type(issues).__name__}")
        for i, issue in enumerate(issues):
            if not isinstance(issue, dict):
                raise AllureValidationError(f"Issue at index {i} must be a dictionary")
            if not issue:
                raise AllureValidationError(f"Issue at index {i} cannot be empty")

    @staticmethod
    def _build_tag_dtos(tags: list[str] | None) -> list[LaunchTagDto] | None:
        if not tags:
            return None
        return [LaunchTagDto(name=tag) for tag in tags]

    @staticmethod
    def _build_link_dtos(links: list[dict[str, str]] | None) -> list[ExternalLinkDto] | None:
        if not links:
            return None
        return [ExternalLinkDto(**link) for link in links]

    @staticmethod
    def _build_issue_dtos(issues: list[dict[str, object]] | None) -> list[IssueDto] | None:
        if not issues:
            return None
        return [IssueDto(**issue) for issue in issues]
