from datetime import date as Date
from typing import List, Optional

from pydantic import BaseModel, Field, computed_field, field_validator

from gpustack.api.exceptions import InvalidException
from gpustack.schemas.common import Pagination


USAGE_METRIC_INPUT_TOKENS = "input_tokens"
USAGE_METRIC_OUTPUT_TOKENS = "output_tokens"
USAGE_METRIC_INPUT_CACHED_TOKENS = "input_cached_tokens"
USAGE_METRIC_TOTAL_TOKENS = "total_tokens"
USAGE_METRIC_API_REQUESTS = "api_requests"
USAGE_METRIC_MODELS_CALLED = "models_called"
USAGE_METRIC_API_KEYS_USED = "api_keys_used"
USAGE_METRIC_AVG_TOKENS_PER_REQUEST = "avg_tokens_per_request"
USAGE_METRIC_LAST_ACTIVE = "last_active"
USAGE_METRIC_DATE = "date"

USAGE_GROUP_BY_DATE = "date"
USAGE_GROUP_BY_MODEL = "model"
USAGE_GROUP_BY_USER = "user"
USAGE_GROUP_BY_API_KEY = "api_key"

USAGE_GRANULARITY_DAY = "day"
USAGE_GRANULARITY_WEEK = "week"
USAGE_GRANULARITY_MONTH = "month"

USAGE_SORT_ASC = "asc"
USAGE_SORT_DESC = "desc"

# Usage view scope. ``self`` filters to the caller's own rows
# (``user_id = self``); ``all`` filters to the current Org's rows
# (``owner_principal_id = current_principal_id``), or — for platform admin in
# cross-org context — to every Org. ``all`` is reserved for admin /
# Org owner / manager; others are forced to ``self``.
USAGE_SCOPE_SELF = "self"
USAGE_SCOPE_ALL = "all"
USAGE_SCOPES = {USAGE_SCOPE_SELF, USAGE_SCOPE_ALL}

USAGE_GROUP_BYS = {
    USAGE_GROUP_BY_DATE,
    USAGE_GROUP_BY_MODEL,
    USAGE_GROUP_BY_USER,
    USAGE_GROUP_BY_API_KEY,
}
USAGE_GRANULARITIES = {
    USAGE_GRANULARITY_DAY,
    USAGE_GRANULARITY_WEEK,
    USAGE_GRANULARITY_MONTH,
}
USAGE_SORTABLE_FIELDS = {
    USAGE_METRIC_INPUT_TOKENS,
    USAGE_METRIC_OUTPUT_TOKENS,
    USAGE_METRIC_INPUT_CACHED_TOKENS,
    USAGE_METRIC_TOTAL_TOKENS,
    USAGE_METRIC_API_REQUESTS,
    USAGE_METRIC_AVG_TOKENS_PER_REQUEST,
    USAGE_METRIC_MODELS_CALLED,
    USAGE_METRIC_API_KEYS_USED,
    USAGE_METRIC_LAST_ACTIVE,
    USAGE_METRIC_DATE,
}


class UsageOption(BaseModel):
    key: str
    label: str


class UsageIdentityValue(BaseModel):
    cluster_name: Optional[str] = None
    model_name: Optional[str] = None
    provider_name: Optional[str] = None
    provider_type: Optional[str] = None
    user_name: Optional[str] = None
    api_key_name: Optional[str] = None
    access_key: Optional[str] = None
    api_key_is_custom: Optional[bool] = None


class UsageIdentityCurrent(BaseModel):
    model_id: Optional[int] = None
    provider_id: Optional[int] = None
    user_id: Optional[int] = None
    api_key_id: Optional[int] = None


class UsageIdentity(BaseModel):
    value: UsageIdentityValue
    current: Optional[UsageIdentityCurrent] = None


class UsageFilterItem(BaseModel):
    identity: UsageIdentity


class UsageFilterOption(UsageFilterItem):
    label: str
    deleted: bool


class UsageFilters(BaseModel):
    models: List[UsageFilterOption] = Field(default_factory=list)
    users: List[UsageFilterOption] = Field(default_factory=list)
    api_keys: List[UsageFilterOption] = Field(default_factory=list)


class UsageMetaResponse(BaseModel):
    metrics: List[UsageOption]
    granularities: List[UsageOption]
    group_bys: List[UsageOption]
    filters: UsageFilters


class UsageFilterRequest(BaseModel):
    models: List[UsageFilterItem] = Field(default_factory=list)
    users: List[UsageFilterItem] = Field(default_factory=list)
    api_keys: List[UsageFilterItem] = Field(default_factory=list)


class UsageBaseRequest(BaseModel):
    start_date: Date
    end_date: Date
    filters: UsageFilterRequest = Field(default_factory=UsageFilterRequest)
    # See USAGE_SCOPE_* constants. Defaults to "all" so that managers /
    # admins who omit the parameter get the org-wide provider view; the
    # endpoint downgrades to "self" automatically when the caller has
    # no managerial role (and rejects the request if they explicitly
    # asked for "all").
    scope: str = USAGE_SCOPE_ALL

    @field_validator("end_date")
    @classmethod
    def validate_date_range(cls, value: Date, info) -> Date:
        start_date = info.data.get("start_date")
        if start_date and value < start_date:
            raise ValueError("end_date must be on or after start_date")
        return value

    @field_validator("scope")
    @classmethod
    def validate_scope(cls, value: str) -> str:
        if value not in USAGE_SCOPES:
            raise ValueError(f"Unsupported scope: {value}")
        return value


class UsageBreakdownRequest(UsageBaseRequest):
    group_by: List[str]
    granularity: Optional[str] = None
    sort_by: Optional[str] = f"-{USAGE_METRIC_TOTAL_TOKENS}"
    page: int = 1
    perPage: int = 20

    @field_validator("group_by")
    @classmethod
    def validate_group_by(cls, value: List[str]) -> List[str]:
        if not value:
            raise ValueError("group_by must not be empty")
        unsupported = [item for item in value if item not in USAGE_GROUP_BYS]
        if unsupported:
            raise ValueError(f"Unsupported group_by: {', '.join(unsupported)}")
        if len(value) != len(set(value)):
            raise ValueError("group_by must not contain duplicate values")
        return value

    @field_validator("granularity")
    @classmethod
    def validate_granularity(cls, value: Optional[str]) -> Optional[str]:
        if value is not None and value not in USAGE_GRANULARITIES:
            raise ValueError(f"Unsupported granularity: {value}")
        return value

    @field_validator("sort_by")
    @classmethod
    def validate_sort_by(cls, value: Optional[str]) -> Optional[str]:
        if not value:
            return value
        for field in value.split(","):
            field = field.strip()
            if not field:
                continue
            field_name = field[1:] if field.startswith("-") else field
            if field_name not in USAGE_SORTABLE_FIELDS:
                raise InvalidException(
                    f"Field '{field_name}' is not sortable. "
                    f"Allowed fields: {', '.join(sorted(USAGE_SORTABLE_FIELDS))}"
                )
        return value

    @field_validator("page", "perPage")
    @classmethod
    def validate_positive_int(cls, value: int) -> int:
        if value < 1:
            raise ValueError("page and perPage must be positive")
        return value

    @computed_field
    @property
    def order_by(self) -> List[tuple[str, str]]:
        if not self.sort_by:
            return []
        order_by = []
        for field in self.sort_by.split(","):
            field = field.strip()
            if not field:
                continue
            if field.startswith("-"):
                order_by.append((field[1:], USAGE_SORT_DESC))
            else:
                order_by.append((field, USAGE_SORT_ASC))
        return order_by


class UsageSummary(BaseModel):
    input_tokens: int = 0
    output_tokens: int = 0
    input_cached_tokens: int = 0
    total_tokens: int = 0
    api_requests: int = 0
    models_called: int = 0


class UsageBreakdownDimension(BaseModel):
    identity: Optional[UsageIdentity] = None
    label: str
    deleted: bool


class UsageBreakdownDateDimension(BaseModel):
    value: Date
    label: str
    deleted: bool = False


class UsageBreakdownItem(BaseModel):
    date: Optional[UsageBreakdownDateDimension] = None
    model: Optional[UsageBreakdownDimension] = None
    user: Optional[UsageBreakdownDimension] = None
    api_key: Optional[UsageBreakdownDimension] = None
    input_tokens: int = 0
    output_tokens: int = 0
    input_cached_tokens: int = 0
    total_tokens: int = 0
    api_requests: int = 0
    avg_tokens_per_request: float = 0
    models_called: Optional[int] = None
    api_keys_used: Optional[int] = None
    last_active: Optional[Date] = None


class UsageBreakdownResponse(BaseModel):
    summary: UsageSummary
    group_by: List[str]
    granularity: Optional[str] = None
    pagination: Pagination
    items: List[UsageBreakdownItem]
