import datetime

import requests
import json
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Any


@dataclass
class SearchPagination:
  currentPage: int
  pageSize: int
  totalPages: int
  totalResults: int


@dataclass
class ScoreSearchResult:
  user_id: int
  user_username: str
  user_join_date: str
  user_country: str
  user_rank: int
  user_pp_raw: float
  user_accuracy: float
  user_playcount: int
  user_total_score: int
  user_ranked_score: int
  user_seconds_played: int
  user_count_300: int
  user_count_100: int
  user_count_50: int
  user_count_miss: int
  user_is_banned: bool
  id: int
  beatmap_id: int
  count_300: int
  count_100: int
  count_50: int
  count_miss: int
  date: str
  max_combo: int
  mods: int
  perfect: bool
  pp: float
  rank: str
  replay_id: int
  score: int
  ur: float
  frametime: float
  edge_hits: int
  snaps: int
  adjusted_ur: float
  mean_error: float
  error_variance: float
  error_standard_deviation: float
  minimum_error: int
  maximum_error: int
  error_range: int
  error_coefficient_of_variation: float
  error_kurtosis: float
  error_skewness: float
  keypresses_median_adjusted: float
  keypresses_standard_deviation_adjusted: float
  sliderend_release_median_adjusted: float
  sliderend_release_standard_deviation_adjusted: float
  beatmap_artist: str
  beatmap_beatmapset_id: int
  beatmap_creator: str
  beatmap_source: Optional[str]
  beatmap_star_rating: float
  beatmap_title: str
  beatmap_version: str


@dataclass
class ScoreSearchResult:
  pagination: SearchPagination
  results: List[ScoreSearchResult]


@dataclass
class UserSearchResult:
  user_id: int
  user_username: str
  user_join_date: str
  user_country: str
  user_rank: int
  user_pp_raw: float
  user_accuracy: float
  user_playcount: int
  user_total_score: int
  user_ranked_score: int
  user_seconds_played: int
  user_count_300: int
  user_count_100: int
  user_count_50: int
  user_count_miss: int
  user_is_banned: bool


@dataclass
class UserSearchResult:
  pagination: SearchPagination
  results: List[UserSearchResult]


@dataclass
class ErrorDistribution:
  countMiss: float
  count300: float
  count100: float
  count50: float


@dataclass
class ReplayData:
  replay_id: int
  user_id: int
  username: str
  date: str
  beatmap_id: int
  beatmap_beatmapset_id: int
  beatmap_artist: str
  beatmap_title: str
  beatmap_star_rating: float
  beatmap_creator: str
  beatmap_version: str
  beatmap_max_combo: int
  beatmap_total_length: int
  beatmap_bpm: float
  beatmap_accuracy: float
  beatmap_ar: float
  beatmap_cs: float
  beatmap_drain: float
  beatmap_count_circles: int
  beatmap_count_sliders: int
  beatmap_count_spinners: int
  score: int
  mods: List[str]
  rank: str
  ur: float
  adjusted_ur: float
  frametime: int
  snaps: int
  hits: int
  mean_error: float
  error_variance: float
  error_standard_deviation: float
  minimum_error: float
  maximum_error: float
  error_range: float
  error_coefficient_of_variation: float
  error_kurtosis: float
  error_skewness: float
  comparable_samples: int
  comparable_mean_error: float
  comparable_error_variance: float
  comparable_error_standard_deviation: float
  comparable_minimum_error: float
  comparable_maximum_error: float
  comparable_error_range: float
  comparable_error_coefficient_of_variation: float
  comparable_error_kurtosis: float
  comparable_error_skewness: float
  comparable_adjusted_ur: float
  pp: float
  perfect: bool
  max_combo: int
  count_300: int
  count_100: int
  count_50: int
  count_miss: int
  error_distribution: Dict[str, ErrorDistribution]
  similar_scores: List[Any]
  charts: Any


@dataclass
class UserDetails:
  user_id: int
  username: str
  rank: int
  pp_raw: float
  join_date: str
  seconds_played: int
  country: str
  country_rank: Optional[int]
  playcount: int
  suspicious_scores: int
  stolen_replays: int


@dataclass
class QueueDetails:
  isProcessing: bool
  lastCompletedUpdate: Optional[str]
  canUpdate: bool
  progressCurrent: Optional[int]
  progressTotal: Optional[int]


@dataclass
class UserProfile:
  user_details: UserDetails
  queue_details: QueueDetails
  suspicious_scores: List[dict] = field(default_factory=list)
  similar_replays: List[dict] = field(default_factory=list)
  total_scores: int = 0
  is_banned: bool = False
  approximate_ban_date: Optional[str] = None


@dataclass
class SuspiciousScoreEntry:
  user_id: int
  username: str
  replay_id: int
  date: str
  beatmap_id: int
  beatmap_beatmapset_id: int
  beatmap_title: str
  beatmap_star_rating: float
  pp: float
  frametime: float
  ur: float


@dataclass
class StolenReplayEntry:
  replay_id_1: int
  replay_id_2: int
  user_id_1: int
  user_id_2: int
  username_1: str
  username_2: str
  beatmap_beatmapset_id: int
  replay_date_1: str
  replay_date_2: str
  replay_pp_1: float
  replay_pp_2: float
  beatmap_id: int
  beatmap_title: str
  beatmap_star_rating: float
  similarity: float


@dataclass
class BanListPagination:
  currentPage: int
  pageSize: int
  totalPages: int
  totalResults: int


@dataclass
class BanListEntry:
  userId: int
  username: str
  secondsPlayed: int
  pp: float
  rank: int
  isBanned: bool
  approximateBanTime: datetime
  lastUpdate: datetime


@dataclass
class BanList:
  pagination: BanListPagination
  users: List[BanListEntry]


class NiseAPI:
  def __init__(self, base_url="https://nise.moe"):
    self.base_url = base_url
    self.headers = {
      "X-NISE-API": "20240218",
      "Accept": "application/json",
      "Content-Type": "application/json"
    }

  def search_scores(self, query) -> ScoreSearchResult:
    url = f"{self.base_url}/api/search"
    response = requests.post(url, headers=self.headers, data=json.dumps(query))
    return ScoreSearchResult(**response.json())

  def search_users(self, query) -> UserSearchResult:
    url = f"{self.base_url}/api/search-user"
    response = requests.post(url, headers=self.headers, data=json.dumps(query))
    return UserSearchResult(**response.json())

  def get_single_score(self, replay_id) -> ReplayData:
    url = f"{self.base_url}/api/score/{replay_id}"
    response = requests.get(url, headers=self.headers)
    try:
      return ReplayData(**response.json())
    except (TypeError, ValueError):
      raise ValueError()

  def get_user_details(self, user_id=None, username=None) -> UserProfile:
    url = f"{self.base_url}/api/user-details"
    data = {}
    if user_id:
      data["userId"] = user_id
    elif username:
      data["username"] = username
    response = requests.post(url, headers=self.headers, data=json.dumps(data))
    try:
      return UserProfile(**response.json())
    except (TypeError, ValueError):
      raise ValueError()

  def get_suspicious_scores(self) -> List[SuspiciousScoreEntry]:
    url = f"{self.base_url}/api/suspicious-scores"
    response = requests.get(url, headers=self.headers)
    return [SuspiciousScoreEntry(**item) for item in response.json()]

  def get_similar_replays(self) -> List[StolenReplayEntry]:
    url = f"{self.base_url}/api/similar-replays"
    response = requests.get(url, headers=self.headers)
    return [StolenReplayEntry(**item) for item in response.json()]

  def get_banlist(self, page) -> BanList:
    url = f"{self.base_url}/api/banlist"
    data = {"page": page}
    response = requests.post(url, headers=self.headers, data=json.dumps(data))
    return BanList(**response.json())
