from collections.abc import Iterator

from pydantic import validate_call

from albert.collections.base import BaseCollection
from albert.core.logging import logger
from albert.core.pagination import AlbertPaginator, PaginationMode
from albert.core.session import AlbertSession
from albert.core.shared.identifiers import CompanyId
from albert.core.utils import ensure_list
from albert.exceptions import AlbertException
from albert.resources.companies import Company


class CompanyCollection(BaseCollection):
    """
    CompanyCollection is a collection class for managing Company entities in the Albert platform.
    """

    _updatable_attributes = {"name"}
    _api_version = "v3"

    def __init__(self, *, session: AlbertSession):
        """
        Initializes the CompanyCollection with the provided session.

        Parameters
        ----------
        session : AlbertSession
            The Albert session instance.
        """
        super().__init__(session=session)
        self.base_path = f"/api/{CompanyCollection._api_version}/companies"

    def get_all(
        self,
        *,
        name: str | list[str] = None,
        exact_match: bool = True,
        start_key: str | None = None,
        max_items: int | None = None,
    ) -> Iterator[Company]:
        """
        Get all company entities with optional filters.

        Parameters
        ----------
        name : str | list[str], optional
            The name(s) of the company to filter by.
        exact_match : bool, optional
            Whether to match the name(s) exactly. Default is True.
        start_key : str, optional
            Key to start paginated results from.
        max_items : int, optional
            Maximum number of items to return in total. If None, fetches all available items.

        Returns
        -------
        Iterator[Company]
            An iterator of Company entities.
        """
        params = {
            "dupDetection": "false",
            "startKey": start_key,
        }
        params["name"] = ensure_list(name)
        params["exactMatch"] = str(exact_match).lower()

        return AlbertPaginator(
            mode=PaginationMode.KEY,
            path=self.base_path,
            session=self.session,
            params=params,
            max_items=max_items,
            deserialize=lambda items: [Company(**item) for item in items],
        )

    def exists(self, *, name: str, exact_match: bool = True) -> bool:
        """
        Checks if a company exists by its name.

        Parameters
        ----------
        name : str
            The name of the company to check.
        exact_match : bool, optional
            Whether to match the name exactly, by default True.

        Returns
        -------
        bool
            True if the company exists, False otherwise.
        """
        companies = self.get_by_name(name=name, exact_match=exact_match)
        return bool(companies)

    @validate_call
    def get_by_id(self, *, id: CompanyId) -> Company:
        """
        Get a company by its ID.

        Parameters
        ----------
        id : str
            The ID of the company to retrieve.

        Returns
        -------
        Company
            The Company object.
        """
        url = f"{self.base_path}/{id}"
        response = self.session.get(url)
        company = response.json()
        found_company = Company(**company)
        return found_company

    def get_by_name(self, *, name: str, exact_match: bool = True) -> Company | None:
        """
        Retrieves a company by its name.

        Parameters
        ----------
        name : str
            The name of the company to retrieve.
        exact_match : bool, optional
            Whether to match the name exactly, by default True.

        Returns
        -------
        Company
            The Company object if found, None otherwise.
        """
        found = self.get_all(name=name, exact_match=exact_match, max_items=1)
        return next(found, None)

    def create(self, *, company: str | Company) -> Company:
        """
        Creates a new company entity.

        Parameters
        ----------
        company : Union[str, Company]
            The company name or Company object to create.

        Returns
        -------
        Company
            The created Company object.
        """
        if isinstance(company, str):
            company = Company(name=company)

        payload = company.model_dump(by_alias=True, exclude_unset=True, mode="json")
        response = self.session.post(self.base_path, json=payload)
        this_company = Company(**response.json())
        return this_company

    def get_or_create(self, *, company: str | Company) -> Company:
        """
        Retrieves a company by its name or creates it if it does not exist.

        Parameters
        ----------
        company : Union[str, Company]
            The company name or Company object to retrieve or create.

        Returns
        -------
        Company
            The Company object if found or created.
        """
        if isinstance(company, str):
            company = Company(name=company)
        found = self.get_by_name(name=company.name, exact_match=True)
        if found:
            return found
        else:
            return self.create(company=company)

    @validate_call
    def merge(
        self,
        *,
        parent_id: CompanyId,
        child_ids: CompanyId | list[CompanyId],
    ) -> Company:
        """
        Merge one or more child companies into a parent company.

        Parameters
        ----------
        parent_id : CompanyId
            The ID of the parent company.
        child_ids : CompanyId | list[CompanyId]
            A single child company ID or a list of child company IDs to merge.

        Returns
        -------
        Company
            The updated parent company after the merge operation.
        """

        child_ids = [child_ids] if isinstance(child_ids, str) else list(child_ids)

        url = f"{self.base_path}/merge"
        payload = {
            "parentId": parent_id,
            "ChildCompanies": [{"id": cid} for cid in child_ids],
        }
        response = self.session.post(url, json=payload)
        if response.status_code == 206:
            details = response.json()
            logger.warning("Company merge partially succeeded", extra={"details": details})
        return self.get_by_id(id=parent_id)

    @validate_call
    def delete(self, *, id: CompanyId) -> None:
        """Deletes a company entity.

        Parameters
        ----------
        id : str
            The ID of the company to delete.

        Returns
        -------
        None
        """
        url = f"{self.base_path}/{id}"
        self.session.delete(url)

    def rename(self, *, old_name: str, new_name: str) -> Company:
        """
        Renames an existing company entity.

        Parameters
        ----------
        old_name : str
            The current name of the company.
        new_name : str
            The new name of the company.

        Returns
        -------
        Company
            The renamed Company object
        """
        company = self.get_by_name(name=old_name, exact_match=True)
        if not company:
            msg = f'Company "{old_name}" not found.'
            logger.error(msg)
            raise AlbertException(msg)
        company_id = company.id
        endpoint = f"{self.base_path}/{company_id}"
        payload = {
            "data": [
                {
                    "operation": "update",
                    "attribute": "name",
                    "oldValue": old_name,
                    "newValue": new_name,
                }
            ]
        }
        self.session.patch(endpoint, json=payload)
        updated_company = self.get_by_id(id=company_id)
        return updated_company

    def update(self, *, company: Company) -> Company:
        """Update a Company entity. The id of the company must be provided.

        Parameters
        ----------
        company : Company
            The updated Company object.

        Returns
        -------
        Company
            The updated Company object as registered in Albert.
        """
        # Fetch the current object state from the server or database
        current_object = self.get_by_id(id=company.id)

        # Generate the PATCH payload
        patch_payload = self._generate_patch_payload(existing=current_object, updated=company)
        url = f"{self.base_path}/{company.id}"
        self.session.patch(url, json=patch_payload.model_dump(mode="json", by_alias=True))
        updated_company = self.get_by_id(id=company.id)
        return updated_company
