"""
Organization tag CRUD operations
"""

from typing import Dict, Any, List, Optional
from ..models import Tag, TallyfyError
from ..validation import validate_id


class TagManager:
    """Handles organization tag CRUD operations"""

    def __init__(self, sdk):
        self.sdk = sdk

    def _validate_org_id(self, org_id: str) -> None:
        validate_id(org_id, "org_id")

    def _validate_tag_id(self, tag_id: str) -> None:
        validate_id(tag_id, "tag_id")

    def _handle_api_error(self, error: Exception, operation: str, **context) -> None:
        context_str = ", ".join(f"{k}={v}" for k, v in context.items())
        msg = f"Failed to {operation}"
        if context_str:
            msg += f" ({context_str})"
        msg += f": {error}"
        self.sdk.logger.error(msg)
        if isinstance(error, TallyfyError):
            raise error
        raise TallyfyError(msg)

    def _parse_tag(self, response_data: Any) -> Tag:
        """Extract a Tag from a wrapped or bare API response."""
        if isinstance(response_data, dict) and 'data' in response_data:
            data = response_data['data']
            if isinstance(data, dict):
                return Tag.from_dict(data)
        if isinstance(response_data, dict):
            return Tag.from_dict(response_data)
        raise TallyfyError("Unexpected response format")

    # ── READ ──────────────────────────────────────────────────────────────

    def get_tags(self, org_id: str, page: int = 1, per_page: int = 100,
                 q: Optional[str] = None, sort: Optional[str] = None,
                 status: Optional[str] = None,
                 auto_generated: Optional[bool] = None) -> List[Tag]:
        """
        List all tags in the organization.

        Args:
            org_id: Organization ID
            page: Page number (default: 1)
            per_page: Results per page (default: 100)
            q: Search query
            sort: Sort direction
            status: Filter by status
            auto_generated: Filter by auto-generated flag

        Returns:
            List of Tag objects

        Raises:
            TallyfyError: If the request fails
        """
        self._validate_org_id(org_id)

        try:
            endpoint = f"organizations/{org_id}/tags"
            params: Dict[str, Any] = {"page": page, "per_page": per_page}
            if q is not None:
                params["q"] = q
            if sort is not None:
                params["sort"] = sort
            if status is not None:
                params["status"] = status
            if auto_generated is not None:
                params["auto_generated"] = auto_generated

            response_data = self.sdk._make_request('GET', endpoint, params=params)

            if isinstance(response_data, dict) and 'data' in response_data:
                items = response_data['data']
            elif isinstance(response_data, list):
                items = response_data
            else:
                items = []

            return [Tag.from_dict(item) for item in items if isinstance(item, dict)]

        except TallyfyError:
            raise
        except Exception as e:
            self._handle_api_error(e, "get tags", org_id=org_id)

    def get_tag(self, org_id: str, tag_id: str) -> Tag:
        """
        Get a single tag by ID.

        Args:
            org_id: Organization ID
            tag_id: Tag ID

        Returns:
            Tag object

        Raises:
            TallyfyError: If the request fails
        """
        self._validate_org_id(org_id)
        self._validate_tag_id(tag_id)

        try:
            endpoint = f"organizations/{org_id}/tags/{tag_id}"
            response_data = self.sdk._make_request('GET', endpoint)
            return self._parse_tag(response_data)

        except TallyfyError:
            raise
        except Exception as e:
            self._handle_api_error(e, "get tag", org_id=org_id, tag_id=tag_id)

    # ── CREATE ────────────────────────────────────────────────────────────

    def create_tag(self, org_id: str, title: str,
                   color: Optional[str] = None) -> Tag:
        """
        Create a new tag in the organization.

        Args:
            org_id: Organization ID
            title: Tag title (required)
            color: Optional hex color string (e.g. "#FF5733")

        Returns:
            Created Tag object

        Raises:
            TallyfyError: If the request fails
            ValueError: If required parameters are missing
        """
        self._validate_org_id(org_id)

        if not title or not isinstance(title, str):
            raise ValueError("title must be a non-empty string")

        try:
            endpoint = f"organizations/{org_id}/tags"
            body: Dict[str, Any] = {"title": title}
            if color is not None:
                body["color"] = color

            response_data = self.sdk._make_request('POST', endpoint, data=body)
            return self._parse_tag(response_data)

        except TallyfyError:
            raise
        except Exception as e:
            self._handle_api_error(e, "create tag", org_id=org_id, title=title)

    # ── UPDATE ────────────────────────────────────────────────────────────

    def update_tag(self, org_id: str, tag_id: str,
                   title: Optional[str] = None,
                   color: Optional[str] = None) -> Tag:
        """
        Update a tag's properties.

        Args:
            org_id: Organization ID
            tag_id: Tag ID to update
            title: New tag title
            color: New hex color string (e.g. "#00FF00")

        Returns:
            Updated Tag object

        Raises:
            TallyfyError: If the request fails
            ValueError: If required parameters are missing
        """
        self._validate_org_id(org_id)
        self._validate_tag_id(tag_id)

        if title is None and color is None:
            raise ValueError("At least one of title or color must be provided")

        try:
            endpoint = f"organizations/{org_id}/tags/{tag_id}"
            body: Dict[str, Any] = {}
            if title is not None:
                body["title"] = title
            if color is not None:
                body["color"] = color

            response_data = self.sdk._make_request('PUT', endpoint, data=body)
            return self._parse_tag(response_data)

        except TallyfyError:
            raise
        except Exception as e:
            self._handle_api_error(e, "update tag", org_id=org_id, tag_id=tag_id)

    # ── TEMPLATE TAGGING ──────────────────────────────────────────────────

    def tag_template(self, org_id: str, template_id: str, tag_id: str) -> bool:
        """
        Attach a tag to a template.

        Args:
            org_id: Organization ID
            template_id: Template (checklist) ID to tag
            tag_id: Tag ID to attach

        Returns:
            True if tagged successfully

        Raises:
            TallyfyError: If the request fails
        """
        self._validate_org_id(org_id)
        self._validate_tag_id(tag_id)

        if not template_id or not isinstance(template_id, str):
            raise ValueError("template_id must be a non-empty string")

        try:
            endpoint = f"organizations/{org_id}/checklists/tags"
            body: Dict[str, Any] = {
                "subject_id": template_id,
                "subject_type": "Checklist",
                "tag_id": tag_id,
                "tag_type": "private",
            }
            self.sdk._make_request('POST', endpoint, data=body)
            return True

        except TallyfyError:
            raise
        except Exception as e:
            self._handle_api_error(e, "tag template", org_id=org_id, template_id=template_id, tag_id=tag_id)

    def untag_template(self, org_id: str, tag_id: str, template_id: Optional[str] = None) -> bool:
        """
        Remove a tag from a template.

        Args:
            org_id: Organization ID
            tag_id: Tag ID to remove
            template_id: Template ID (required to look up the tag association ID)

        Returns:
            True if untagged successfully

        Raises:
            TallyfyError: If the request fails
        """
        self._validate_org_id(org_id)
        self._validate_tag_id(tag_id)

        try:
            association_id = tag_id

            # If template_id is provided, look up the tag-template association ID
            if template_id:
                tmpl_endpoint = f"organizations/{org_id}/checklists/{template_id}"
                tmpl_response = self.sdk._make_request('GET', tmpl_endpoint, params={'with': 'tags'})
                if isinstance(tmpl_response, dict):
                    tmpl_data = tmpl_response.get('data', tmpl_response)
                    tags = tmpl_data.get('tags') or []
                    # Tags include may be wrapped in {"data": [...]}
                    if isinstance(tags, dict) and 'data' in tags:
                        tags = tags['data']
                    for assoc in tags:
                        if isinstance(assoc, dict) and str(assoc.get('tag_id')) == str(tag_id):
                            association_id = str(assoc.get('id', tag_id))
                            break

            endpoint = f"organizations/{org_id}/checklists/tags/{association_id}"
            self.sdk._make_request('DELETE', endpoint)
            return True

        except TallyfyError:
            raise
        except Exception as e:
            self._handle_api_error(e, "untag template", org_id=org_id, tag_id=tag_id)

    # ── PROCESS TAGGING ──────────────────────────────────────────────────

    def tag_process(self, org_id: str, process_id: str, tag_id: str) -> bool:
        """
        Attach a tag to a process run.

        Args:
            org_id: Organization ID
            process_id: Process (run) ID to tag
            tag_id: Tag ID to attach

        Returns:
            True if tagged successfully

        Raises:
            TallyfyError: If the request fails
        """
        self._validate_org_id(org_id)
        self._validate_tag_id(tag_id)

        if not process_id or not isinstance(process_id, str):
            raise ValueError("process_id must be a non-empty string")
        try:
            endpoint = f"organizations/{org_id}/checklists/tags"
            body: Dict[str, Any] = {
                "subject_id": process_id,
                "subject_type": "Run",
                "tag_id": tag_id,
                "tag_type": "private",
            }
            self.sdk._make_request('POST', endpoint, data=body)
            return True

        except TallyfyError:
            raise
        except Exception as e:
            self._handle_api_error(e, "tag process", org_id=org_id, process_id=process_id, tag_id=tag_id)

    def untag_process(self, org_id: str, process_id: str, tag_id: str) -> bool:
        """
        Remove a tag from a process run.

        Looks up the association ID by fetching the process with its tags,
        then deletes the matching tag association.

        Args:
            org_id: Organization ID
            process_id: Process (run) ID to untag
            tag_id: Tag ID to remove

        Returns:
            True if untagged successfully

        Raises:
            TallyfyError: If the request fails
            ValueError: If the tag is not found on the process
        """
        self._validate_org_id(org_id)
        self._validate_tag_id(tag_id)

        if not process_id or not isinstance(process_id, str):
            raise ValueError("process_id must be a non-empty string")

        try:
            # Fetch the process with tags to find the association ID
            run = self.sdk.tasks.get_process(org_id, process_id, with_='tags')

            association_id = None
            tags = run.tags or []
            # Tags include may be wrapped in {"data": [...]}
            if isinstance(tags, dict) and 'data' in tags:
                tags = tags['data']
            for assoc in tags:
                if isinstance(assoc, dict) and str(assoc.get('tag_id')) == str(tag_id):
                    association_id = str(assoc['id'])
                    break

            if not association_id:
                raise ValueError(f"Tag {tag_id} not found on process {process_id}")

            endpoint = f"organizations/{org_id}/checklists/tags/{association_id}"
            self.sdk._make_request('DELETE', endpoint)
            return True

        except (TallyfyError, ValueError):
            raise
        except Exception as e:
            self._handle_api_error(e, "untag process", org_id=org_id,
                                   process_id=process_id, tag_id=tag_id)

    # ── DELETE ────────────────────────────────────────────────────────────

    def delete_tag(self, org_id: str, tag_id: str) -> bool:
        """
        Delete a tag.

        Args:
            org_id: Organization ID
            tag_id: Tag ID to delete

        Returns:
            True if deleted successfully

        Raises:
            TallyfyError: If the request fails
            ValueError: If required parameters are missing
        """
        self._validate_org_id(org_id)
        self._validate_tag_id(tag_id)

        try:
            endpoint = f"organizations/{org_id}/tags/{tag_id}"
            self.sdk._make_request('DELETE', endpoint)
            return True

        except TallyfyError:
            raise
        except Exception as e:
            self._handle_api_error(e, "delete tag", org_id=org_id, tag_id=tag_id)
