# Copyright (c) CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
#

"""
V4 Organizations API Client

This module provides a clean, modern interface for managing organization data using the v4 API
with Okta authentication.

Functions:
- get_organizations() -> returns Dict[str, OrganizationDetails]
- get_organization_tags(organization_id) -> returns Dict[str, str]

V4 API specification compliance:
- Uses V4 API fields: cz_organization_name, cz_organization_id
- No backward compatibility with legacy field names
- Implements standardized V4 error handling
- Supports all documented query parameters
"""

from typing import Any

from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
from rich.console import Console

from src.api.authenticated import get_authenticated, post_authenticated
from src.exceptions import APIError
from src.schemas.v4_errors import V4ErrorResponse, is_v4_error_response

console = Console()


class DictAccessModel(BaseModel):
    """Base model that supports dict-style access (.get(), [key]) alongside attribute access."""

    def get(self, key: str, default: Any = None) -> Any:
        return getattr(self, key, default)

    def __getitem__(self, key: str) -> Any:
        try:
            return getattr(self, key)
        except AttributeError as err:
            raise KeyError(key) from err

    def __contains__(self, key: str) -> bool:
        return hasattr(self, key)


class ConnectionsSummary(DictAccessModel):
    """Aggregate connection counts for an organization (no per-connection detail)."""

    total_connections: int = Field(default=0)
    total_enabled_connections: int = Field(default=0)
    total_billed_spend_30_days: str | None = Field(None)
    enabled_providers: list[str] = Field(default_factory=list)
    all_providers: list[str] = Field(default_factory=list)
    type_counts: dict[str, int] = Field(default_factory=dict)
    auto_link_enabled: bool = Field(default=False, description="Whether any connection has auto-link enabled")

    model_config = ConfigDict(extra="allow")


# V4 Pydantic Models matching backend OrganizationDetails schema
class OrganizationDetails(DictAccessModel):
    """V4 organization schema matching backend API contract."""

    # Required Fields
    cz_organization_id: str = Field(..., description="CloudZero organization UUID")
    cz_organization_name: str = Field(..., description="Organization display name")
    cz_organization_name_normalized: str | None = Field(None, description="Lowercased organization name for searching")
    organization_enabled: bool = Field(..., description="Whether organization is enabled")
    connections_enabled: bool = Field(default=False, description="Whether org has any active connections")
    auto_link_enabled: bool = Field(default=False, description="Whether any connection has auto-link enabled")

    # Aggregate connection counts
    connections_summary: ConnectionsSummary | dict = Field(
        default_factory=dict, description="Aggregate connection counts for this organization"
    )

    # Metadata Fields
    created: str | None = Field(None, description="Creation timestamp (ISO format)")
    last_updated: str | None = Field(None, description="Last modification timestamp (ISO format)")
    first_seen: str | None = Field(None, description="First seen timestamp (ISO format)")
    website: str | None = Field(None, description="Organization website URL")
    organization_stage: str | None = Field(None, description="Lifecycle stage from Snowflake")
    record_sources: list[str] = Field(default_factory=list, description="Data sources that contributed to this record")

    model_config = ConfigDict(extra="ignore")

    @model_validator(mode="before")
    @classmethod
    def parse_connections_summary(cls, values):
        """Parse connections_summary dict into ConnectionsSummary model."""
        if isinstance(values, dict) and "connections_summary" in values:
            summary_raw = values.get("connections_summary", {})
            if isinstance(summary_raw, dict) and summary_raw:
                try:
                    values["connections_summary"] = ConnectionsSummary(**summary_raw)
                except Exception:
                    # If parsing fails, keep as dict for graceful degradation
                    pass
        return values


class OrganizationsListResponse(BaseModel):
    """V4 response model for listing organizations."""

    organizations: dict[str, OrganizationDetails] = Field(
        ..., description="Dictionary of organization_id -> OrganizationDetails"
    )
    total_count: int = Field(..., description="Total number of organizations")
    next_cursor: str | None = Field(None, description="Cursor for next page of results")
    has_more: bool = Field(default=False, description="Whether more pages exist")


class OrganizationTagsResponse(BaseModel):
    """V4 response model for organization tags matching API specification."""

    cz_organization_id: str = Field(..., description="Organization ID (V4 API format)")
    tags: dict[str, str] = Field(..., description="Dictionary of tag key-value pairs")


def get_organizations(
    organization_query: str | None = None,
    debug: bool = False,
    enabled_only: bool | None = None,
    refresh_cache: bool | None = None,
    raw: bool = False,
    **kwargs,
) -> dict[str, OrganizationDetails] | dict:
    """
    Get organizations from v4 API.

    Returns detailed organization data as a dictionary mapping organization IDs to OrganizationDetails objects.

    Args:
        organization_query: Optional specific organization ID to query
        debug: Whether to show debug information
        enabled_only: If True, return only enabled organizations. If False, return all.
                     If None (default), uses API default behavior.
        refresh_cache: If True, force refresh of cache. If None (default), uses API default.
        raw: If True, return raw API response dict without Pydantic parsing.
        **kwargs: Backward compatibility arguments (ignored)

    Returns:
        Dict[str, OrganizationDetails]: Organization ID mapped to organization details (when raw=False)
        dict: Raw API response (when raw=True)

    Raises:
        APIError: If the API request fails
        ValueError: If the response format is invalid
    """
    if debug:
        console.print("[cyan]DEBUG: Fetching organizations via V4 API[/]")

    try:
        # Build endpoint path
        endpoint_path = "organizations"
        if organization_query:
            endpoint_path = f"organizations/{organization_query}"

        # Build query parameters for the API request
        query_params = {}
        if enabled_only is not None:
            query_params["enabled_only"] = "true" if enabled_only else "false"
        if refresh_cache is not None:
            query_params["refresh_cache"] = "true" if refresh_cache else "false"

        if debug and query_params:
            console.print(f"[cyan]DEBUG: Query parameters: {query_params}[/]")

        # Make authenticated request to v4 organizations endpoint
        response_data = get_authenticated(endpoint_path=endpoint_path, params=query_params, debug=debug)

        if debug:
            console.print(f"[cyan]DEBUG: Raw API response: {response_data}[/]")

        # Handle V4 standardized error responses
        if isinstance(response_data, dict) and is_v4_error_response(response_data):
            try:
                error_response = V4ErrorResponse(**response_data)
                raise APIError(f"V4 API Error: {error_response.error}", server_response=response_data)
            except ValidationError as e:
                # Fallback to generic error if V4 error format is invalid
                raise APIError(
                    f"API Error: {response_data.get('error', 'Unknown error')}",
                    server_response=response_data,
                ) from e

        # Handle wrapped error responses from backend
        if isinstance(response_data, dict) and "statusCode" in response_data:
            status = response_data.get("statusCode")
            if status and status >= 400:
                # Check multiple error field patterns the backend may use
                error_msg = (
                    response_data.get("body", {}).get("error")
                    or response_data.get("message")
                    or response_data.get("error")
                    or f"HTTP {status}"
                )
                raise APIError(f"Backend error: {error_msg}", status_code=status, server_response=response_data)
            # Extract body from wrapped response (for 200 responses with body wrapper)
            response_data = response_data.get("body", response_data)

        # Handle single organization response (when organization_query is provided)
        if organization_query and "organization_id" in response_data:
            # Single organization response - wrap in organizations dict format
            response_data = {"organizations": {response_data["organization_id"]: response_data}, "total_count": 1}

        # Handle flat organizations dictionary response (standard list response)
        elif (
            isinstance(response_data, dict)
            and "organizations" not in response_data
            and "total_count" not in response_data
        ):
            # Check if this looks like a flat organizations dictionary (org_id -> org_data)
            # All keys should be UUIDs and all values should be organization objects
            # V4 API uses cz_organization_name field
            is_flat_orgs_dict = all(
                isinstance(key, str) and isinstance(value, dict) and "cz_organization_name" in value
                for key, value in response_data.items()
                if response_data
            )

            if is_flat_orgs_dict:
                # Transform flat dictionary into expected format
                organizations_data = response_data.copy()  # Copy the original data
                response_data = {"organizations": organizations_data, "total_count": len(organizations_data)}
                if debug:
                    console.print(
                        f"[cyan]DEBUG: Transformed flat organizations dictionary with {len(response_data['organizations'])} organizations[/]"
                    )

        # Return raw response if requested (before Pydantic parsing)
        if raw:
            if debug:
                console.print(
                    f"[cyan]DEBUG: Returning raw API response with {len(response_data.get('organizations', {}))} organizations[/]"
                )
            return response_data  # Return complete API response, not just organizations

        # Validate response with Pydantic model
        try:
            if debug:
                console.print(
                    f"[cyan]DEBUG: Attempting to validate response with {len(response_data.get('organizations', {}))} organizations[/]"
                )
            response = OrganizationsListResponse(**response_data)
        except ValidationError as e:
            if debug:
                console.print(f"[red]DEBUG: Response validation failed: {e}[/]")
                console.print(f"[red]DEBUG: Raw response data type: {type(response_data)}[/]")
                if isinstance(response_data, dict):
                    console.print(f"[red]DEBUG: Response data keys: {list(response_data.keys())}[/]")
                    # Show sample organization data for debugging
                    if "organizations" in response_data and response_data["organizations"]:
                        first_org_id = next(iter(response_data["organizations"]))
                        first_org_data = response_data["organizations"][first_org_id]
                        console.print(f"[red]DEBUG: Sample organization data: {first_org_data}[/]")

            # This is a client-side validation error - the API returned data but our model can't parse it
            error_details = str(e)

            if "organizations" in error_details and "Field required" in error_details:
                # Server returned error response instead of data
                raise APIError(
                    f"SERVER-SIDE ERROR: The backend API returned an error response instead of organization data.\n"
                    f"Validation Error: {error_details}\n"
                    f"This indicates a server-side issue that needs to be reported.",
                    server_response=response_data,
                ) from e
            else:
                # Client-side validation error - data format mismatch
                raise APIError(
                    f"CLIENT-SIDE ERROR: The CLI cannot parse the data returned by the API.\n"
                    f"Validation Error: {error_details}\n"
                    f"This indicates a data format mismatch between the CLI and API versions. "
                    f"The API returned valid data but the CLI model is incompatible.",
                    server_response=response_data,
                ) from e

        if debug:
            console.print(f"[green]DEBUG: Successfully retrieved {response.total_count} organizations[/]")

        # Return detailed organization data
        return response.organizations

    except APIError:
        # Re-raise API errors as-is
        raise
    except Exception as e:
        error_msg = f"Failed to get organizations: {str(e)}"
        if debug:
            console.print(f"[red]DEBUG: Unexpected error: {error_msg}[/]")
        raise APIError(error_msg) from e


def get_organization_tags(organization_id: str, debug: bool = False) -> dict[str, str]:
    """
    Get tags for a specific organization.

    Args:
        organization_id: CloudZero organization ID
        debug: Whether to show debug information

    Returns:
        Dict[str, str]: Dictionary of tag key-value pairs

    Raises:
        APIError: If the API request fails
        ValueError: If the organization ID is invalid or response format is invalid
    """
    if not organization_id:
        raise ValueError("Organization ID is required")

    if debug:
        console.print(f"[cyan]DEBUG: Fetching tags for organization {organization_id} via V4 API[/]")

    try:
        # Make authenticated request to v4 organizations tags endpoint
        response_data = get_authenticated(endpoint_path=f"organizations/{organization_id}/tags", debug=debug)

        if debug:
            console.print(f"[cyan]DEBUG: Raw API response: {response_data}[/]")

        # Handle V4 standardized error responses
        if isinstance(response_data, dict) and is_v4_error_response(response_data):
            try:
                error_response = V4ErrorResponse(**response_data)
                raise APIError(f"V4 API Error: {error_response.error}", server_response=response_data)
            except ValidationError as e:
                # Fallback to generic error if V4 error format is invalid
                raise APIError(
                    f"API Error: {response_data.get('error', 'Unknown error')}",
                    server_response=response_data,
                ) from e

        # Validate response with Pydantic model
        try:
            response = OrganizationTagsResponse(**response_data)
        except ValidationError as e:
            if debug:
                console.print(f"[red]DEBUG: Response validation failed: {e}[/]")
            raise APIError(f"Invalid response format: {e}", server_response=response_data) from e

        if debug:
            console.print(
                f"[green]DEBUG: Successfully retrieved {len(response.tags)} tags for organization {organization_id}[/]"
            )

        # Return the tags dict directly
        return response.tags

    except APIError:
        # Re-raise API errors as-is
        raise
    except Exception as e:
        error_msg = f"Failed to get organization tags: {str(e)}"
        if debug:
            console.print(f"[red]DEBUG: Unexpected error: {error_msg}[/]")
        raise APIError(error_msg) from e


def refresh_organization(organization_id: str, async_mode: bool = True, debug: bool = False) -> dict:
    """
    Refresh a single organization's data in ops-services-data.

    Triggers a fast refresh of one organization instead of refreshing all data.
    Useful after connection updates to immediately sync ops-services-data with reactor-accounts.

    Args:
        organization_id: Organization UUID to refresh
        async_mode: Run refresh asynchronously (default: True)
        debug: Whether to show debug information

    Returns:
        dict: Refresh results with org/connection counts and timing info

    Raises:
        APIError: If the API request fails
        ValueError: If the organization ID is invalid
    """
    if not organization_id:
        raise ValueError("Organization ID is required")

    if debug:
        mode_text = "async" if async_mode else "sync"
        console.print(f"[cyan]DEBUG: Refreshing organization {organization_id} ({mode_text} mode)[/]")

    try:
        # Build endpoint with async query parameter
        endpoint = f"organizations/{organization_id}/refresh"
        if async_mode:
            endpoint = f"{endpoint}?async=true"

        # Make authenticated POST request
        response_data = post_authenticated(endpoint_path=endpoint, json_data={}, debug=debug)

        if debug:
            console.print(f"[cyan]DEBUG: Raw API response: {response_data}[/]")

        # Validate response
        if not isinstance(response_data, dict):
            raise APIError(f"Invalid response format: expected dict, got {type(response_data)}")

        if response_data.get("status") == "processing":
            if debug:
                console.print(f"[green]DEBUG: Async refresh started for organization {organization_id}[/]")
        elif response_data.get("status") == "success":
            if debug:
                orgs = response_data.get("organizations_written", 0)
                conns = response_data.get("connections_written", 0)
                duration = response_data.get("duration_ms", 0)
                console.print(f"[green]DEBUG: Refresh completed - {orgs} orgs, {conns} connections in {duration}ms[/]")

        return response_data

    except APIError:
        # Re-raise API errors as-is
        raise
    except Exception as e:
        error_msg = f"Failed to refresh organization: {str(e)}"
        if debug:
            console.print(f"[red]DEBUG: Unexpected error: {error_msg}[/]")
        raise APIError(error_msg) from e
