import abc
from functools import cached_property
from typing import Union, Iterable  # TODO: s pythonem 3.10 přepsat na '|'

import seminar.models as m
from django.db.models import Q, Sum
from seminar.utils import resi_v_rocniku

ROCNIK_ZRUSENI_TEMAT = 25


class FixedIterator:
	def next(self):
		return self.niter.__next__()

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


def body_resitelu(
		za: Union[m.Cislo, m.Rocnik, None] = None,
		do: m.Deadline = None,
		od: m.Deadline = None,
		jen_verejne: bool = True,
		resitele: Iterable[m.Resitel] = None,
		null=0 # Výchozí hodnota, pokud pro daného řešitele nejsou body
) -> dict[int, int]:
	filtr = Q()

	if jen_verejne:
		filtr &= Q(reseni__hodnoceni__deadline_body__verejna_vysledkovka=True)

	# Zjistíme, typ objektu v parametru "za"
	if isinstance(za, m.Rocnik):
		filtr &= Q(reseni__hodnoceni__deadline_body__cislo__rocnik=za)
	elif isinstance(za, m.Cislo):
		filtr &= Q(reseni__hodnoceni__deadline_body__cislo=za)

	if do:
		filtr &= Q(reseni__hodnoceni__deadline_body__deadline__lte=do.deadline)

	if od:
		filtr &= Q(reseni__hodnoceni__deadline_body__deadline__gte=od.deadline)

	resiteleQuery = m.Resitel.objects.all()

	if resitele is not None:
		resitele_id = [r.id for r in resitele]
		resiteleQuery = resiteleQuery.filter(id__in=resitele_id)

	# Přidáme ke každému řešiteli údaj ".body" se součtem jejich bodů
	resitele_s_body = resiteleQuery.annotate(
		body=Sum('reseni__hodnoceni__body', filter=filtr))

	# Teď jen z QuerySetu řešitelů anotovaných body vygenerujeme slovník
	# indexovaný řešitelským id obsahující body.
	# Pokud jsou body None, nahradíme za 0.
	slovnik = {
		int(res.id): (res.body if res.body else null) for res in resitele_s_body
	}
	return slovnik


class Vysledkovka(abc.ABC):
	jen_verejne: bool
	rocnik: m.Rocnik
	do_deadlinu: m.Deadline

	@property
	@abc.abstractmethod
	def aktivni_resitele(self) -> list[m.Resitel]:
		...

	@cached_property
	def resitele_s_body_za_rocnik_setrizeny_seznam(self) -> list[tuple[int, int]]:
		# spočítáme všem řešitelům jejich body za ročník
		resitel_body_za_rocnik_slovnik = body_resitelu(
			resitele=self.aktivni_resitele,
			za=self.rocnik,
			jen_verejne=self.jen_verejne,
			do=self.do_deadlinu
		)

		# zeptáme se na dvojice (řešitel, body) za ročník a setřídíme sestupně
		resitele_s_body_za_rocnik_setrizeny_seznam = sorted(
			resitel_body_za_rocnik_slovnik.items(),
			key=lambda x: x[1], reverse=True
		)

		return resitele_s_body_za_rocnik_setrizeny_seznam

	@cached_property
	def body_za_rocnik_seznamy(self) -> tuple[list[int], list[int]]:
		if len(self.resitele_s_body_za_rocnik_setrizeny_seznam) == 0:
			return [], []
		return tuple(zip(*self.resitele_s_body_za_rocnik_setrizeny_seznam))

	@property
	def setrizeni_resitele_id(self) -> list[int]:
		return self.body_za_rocnik_seznamy[0]

	@property
	def setrizene_body(self) -> list[int]:
		return self.body_za_rocnik_seznamy[1]

	@cached_property
	def resitel_body_odjakziva_slovnik(self) -> dict[int, int]:
		return body_resitelu(jen_verejne=self.jen_verejne, do=self.do_deadlinu)

	@cached_property
	def poradi(self):
		# ze seznamu obsahujícího setřízené body spočítáme sloupec s pořadím
		aktualni_poradi = 1
		sloupec_s_poradim = []

		# seskupíme seznam všech bodů podle hodnot
		for index in range(0, len(self.setrizene_body)):
			# pokud je pořadí větší než číslo řádku, tak jsme vypsali větší rozsah
			# a chceme vypsat už jen prázdné místo, než dojdeme na správný řádek
			if (index + 1) < aktualni_poradi:
				sloupec_s_poradim.append("")
				continue
			velikost_skupiny = 0
			# zjistíme počet po sobě jdoucích stejných hodnot
			while self.setrizene_body[index] == self.setrizene_body[
				index + velikost_skupiny]:
				velikost_skupiny += 1
				# na konci musíme ošetřit přetečení seznamu
				if (index + velikost_skupiny) > len(self.setrizene_body) - 1:
					break
			# pokud je velikost skupiny 1, vypíšu pořadí
			if velikost_skupiny == 1:
				sloupec_s_poradim.append(f"{aktualni_poradi}.")
			# pokud je skupina větší, vypíšu rozsah
			else:
				sloupec_s_poradim.append(
					f"{aktualni_poradi}.–{aktualni_poradi + velikost_skupiny - 1}."
				)
			# zvětšíme aktuální pořadí o tolik, kolik pozic bylo přeskočeno
			aktualni_poradi += velikost_skupiny
		return sloupec_s_poradim


class VysledkovkaRocniku(Vysledkovka):

	def __init__(self, rocnik: m.Rocnik, jen_verejne: bool = True):
		self.rocnik = rocnik
		self.jen_verejne = jen_verejne
		deadliny = m.Deadline.objects.filter(cislo__rocnik=rocnik)
		if jen_verejne:
			deadliny = deadliny.filter(verejna_vysledkovka=True)
		self.do_deadlinu = deadliny.order_by("deadline").last()

	@cached_property
	def aktivni_resitele(self) -> list[m.Resitel]:
		return list(resi_v_rocniku(self.rocnik))

	@cached_property
	def cisla_rocniku(self) -> list[m.Cislo]:
		""" Vrátí všechna čísla daného ročníku. """
		if self.jen_verejne:
			return self.rocnik.verejne_vysledkovky_cisla()
		else:
			return self.rocnik.cisla.all().order_by('poradi')

	@cached_property
	def body_za_cisla_slovnik(self) -> dict[int, dict[int, int]]: # Výstup: m.Cislo.id → ( m.Resitel.id → body )
		# TODO: Body jsou decimal!
		body_cisla_slovnik = dict()
		for cislo in self.cisla_rocniku:
			# získáme body za číslo
			body_za_cislo = body_resitelu(
				za=cislo,
				resitele=self.aktivni_resitele,
				jen_verejne=self.jen_verejne,
				null=""
			)
			body_cisla_slovnik[cislo.id] = body_za_cislo
		return body_cisla_slovnik

	class RadekVysledkovkyRocniku:
		# TODO: přepsat na dataclass
		""" Obsahuje věci, které se hodí vědět při konstruování výsledkovky.
		Umožňuje snazší práci v templatu (lepší, než seznam)."""

		def __init__(self, poradi, resitel, body_cisla_seznam, body_rocnik, body_odjakziva, rok):
			self.poradi = poradi
			self.resitel = resitel
			self.rocnik_resitele = resitel.rocnik(rok)
			self.body_rocnik = body_rocnik
			self.body_celkem_odjakziva = body_odjakziva
			self.body_cisla_seznam = body_cisla_seznam
			self.titul = resitel.get_titul(body_odjakziva)

	@cached_property
	def radky_vysledkovky(self) -> list[RadekVysledkovkyRocniku]:
		radky_vysledkovky = []

		setrizeni_resitele_dict = dict()
		for r in m.Resitel.objects.filter(
				id__in=self.setrizeni_resitele_id
		).select_related('osoba'):
			setrizeni_resitele_dict[r.id] = r

		for i, ar_id in enumerate(self.setrizeni_resitele_id):
			if self.setrizene_body[i] > 0:
				# seznam počtu bodů daného řešitele pro jednotlivá čísla
				body_cisla_seznam = []
				for cislo in self.cisla_rocniku:
					body_cisla_seznam.append(self.body_za_cisla_slovnik[cislo.id][ar_id])

				# Pokud řešitel dostal nějaké body
				if self.resitele_s_body_za_rocnik_setrizeny_seznam[i] != 0:
					# vytáhneme informace pro daného řešitele
					radek = self.RadekVysledkovkyRocniku(
						poradi=self.poradi[i],
						resitel=setrizeni_resitele_dict[ar_id],
						body_cisla_seznam=body_cisla_seznam,
						body_rocnik=self.setrizene_body[i],
						body_odjakziva=self.resitel_body_odjakziva_slovnik[ar_id],
						rok=self.rocnik)  # ročník semináře pro získání ročníku řešitele
					radky_vysledkovky.append(radek)

		return radky_vysledkovky


class VysledkovkaCisla(Vysledkovka):
	def __init__(
			self,
			cislo: m.Cislo,
			jen_verejne: bool = True,
			do_deadlinu: m.Deadline = None
	):
		self.cislo = cislo
		self.rocnik = cislo.rocnik
		self.jen_verejne = jen_verejne
		if do_deadlinu is None:
			do_deadlinu = m.Deadline.objects.filter(cislo=cislo).last()
		self.do_deadlinu = do_deadlinu

	@cached_property
	def aktivni_resitele(self) -> list[m.Resitel]:
		# TODO možná chytřeji vybírat aktivní řešitele
		return list(resi_v_rocniku(self.rocnik))

	@cached_property
	def problemy(self) -> list[m.Problem]:
		""" Vrátí seznam všech problémů s body v daném čísle. """
		return m.Problem.objects.filter(
			hodnoceni__in=m.Hodnoceni.objects.filter(deadline_body__cislo=self.cislo)
		).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem')

	@cached_property
	def hlavni_problemy(self) -> list[m.Problem]:
		""" Vrátí seznam všech problémů, které již nemají nadproblém. """
		# hlavní problémy čísla
		# (mají vlastní sloupeček ve výsledkovce, nemají nadproblém)
		hlavni_problemy = set()
		for p in self.problemy:
			hlavni_problemy.add(p.hlavni_problem) # FIXME: proč tohle nemůže obsahovat reálné instance? Ve výsledkovce by se pak zobrazovaly správné kódy…

		# zunikátnění
		hlavni_problemy = list(hlavni_problemy)
		hlavni_problemy.sort(
			key=lambda k: k.kod_v_rocniku)  # setřídit podle t1, t2, c3, ...

		return hlavni_problemy

	# Není cached, protože si myslím, že queryset lze použít ve for jen jednou.
	@property
	def hodnoceni_do_cisla(self):
		hodnoceni = m.Hodnoceni.objects.prefetch_related('reseni__resitele').select_related('problem', 'reseni')
		if self.jen_verejne:
			hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True)
		return hodnoceni.filter(
			deadline_body__cislo=self.cislo,
			deadline_body__deadline__lte=self.do_deadlinu.deadline,
			body__isnull=False,
		)

	@cached_property
	def sectene_body(self):
		"""
			Sečte body za číslo, hlavní problémy a podproblémy.

			Problém s ID '-1' znamená problémy bez nadproblémů, jež nejsou témata, tj. články, úlohy, konfery, …
		"""

		# Body za číslo
		body_za_cislo = {ar.id: "" for ar in self.aktivni_resitele}

		# Body za hlavní problémy
		body_za_temata = {
			hp.id: {ar.id: "" for ar in self.aktivni_resitele}
			for hp in self.temata_a_spol
		}
		# Ostatní body
		body_za_temata[-1] = {ar.id: "" for ar in self.aktivni_resitele}

		# Body za podproblémy
		body_za_problemy = {
			tema.id: {
				problem.id: {ar.id: "" for ar in self.aktivni_resitele}
				for problem in self.podproblemy[tema.id]
			}
			for tema in self.temata_a_spol
		}
		# Ostatní body
		body_za_problemy[-1] = {
				problem.id: {ar.id: "" for ar in self.aktivni_resitele}
				for problem in self.podproblemy[-1]
		}

		# Sečteme hodnocení
		for hodnoceni in self.hodnoceni_do_cisla:
			prob = hodnoceni.problem.get_real_instance()
			nadproblem = prob.hlavni_problem.id

			# Když nadproblém není "téma", pak je "Ostatní"
			if nadproblem not in body_za_temata:
				nadproblem = -1

			problem_slovnik = body_za_problemy[nadproblem][prob.id]
			nadproblem_slovnik = body_za_temata[nadproblem]

			body = hodnoceni.body

			# Může mít více řešitelů
			for resitel in hodnoceni.reseni.resitele.all():
				if resitel not in self.aktivni_resitele:
					continue
				self.pricti_body(body_za_cislo, resitel, body)
				self.pricti_body(nadproblem_slovnik, resitel, body)
				self.pricti_body(problem_slovnik, resitel, body)
		return body_za_cislo, body_za_temata, body_za_problemy

	@cached_property
	def body_za_temata(self) -> dict[int, dict[int, str]]:
		return self.sectene_body[1]

	@cached_property
	def body_za_cislo(self) -> dict[int, str]:
		return self.sectene_body[0]

	@cached_property
	def problemy_slovnik(self):
		return self.sectene_body[2]

	@cached_property
	def temata_a_spol(self) -> list[m.Problem]:
		if self.rocnik.rocnik < ROCNIK_ZRUSENI_TEMAT:
			return self.hlavni_problemy
		else:
			return list(filter(self.ne_clanek_ne_konfera, self.hlavni_problemy))

	@cached_property
	def je_nejake_ostatni(self):
		return len(self.hlavni_problemy) - len(self.temata_a_spol) > 0

	@cached_property
	def podproblemy(self) -> dict[int, list[m.Problem]]:
		podproblemy = {hp.id: [] for hp in self.temata_a_spol}
		temata_a_spol = set(self.temata_a_spol)
		podproblemy[-1] = []

		for problem in self.problemy:
			h_problem = problem.hlavni_problem
			if h_problem in temata_a_spol:
				podproblemy[h_problem.id].append(problem.get_real_instance())
			else:
				podproblemy[-1].append(problem.get_real_instance())

		for podproblem in podproblemy.keys():
			podproblemy[podproblem] = sorted(podproblemy[podproblem], key=lambda p: p.kod_v_rocniku)
		return podproblemy

	@cached_property
	def podproblemy_seznam(self) -> list[list[m.Problem]]:
		return [self.podproblemy[it.id] for it in self.temata_a_spol] + [self.podproblemy[-1]]

	@cached_property
	def podproblemy_iter(self) -> FixedIterator:
		return FixedIterator(self.podproblemy_seznam.__iter__())

	class RadekVysledkovkyCisla(object):
		# TODO: Přepsat na dataclass
		"""Obsahuje věci, které se hodí vědět při konstruování výsledkovky.
		Umožňuje snazší práci v templatu (lepší, než seznam)."""

		def __init__(self, poradi, resitel, temata_seznamk, body_cislo, body_rocnik, body_odjakziva, rok, body_podproblemy, body_podproblemy_iter):
			self.resitel = resitel
			self.rocnik_resitele = resitel.rocnik(rok)
			self.body_cislo = body_cislo
			self.body_rocnik = body_rocnik
			self.body_celkem_odjakziva = body_odjakziva
			self.poradi = poradi
			self.body_za_temata_seznam = temata_seznamk
			self.titul = resitel.get_titul(body_odjakziva)
			self.body_podproblemy = body_podproblemy
			self.body_podproblemy_iter = body_podproblemy_iter

	@cached_property
	def radky_vysledkovky(self) -> list[RadekVysledkovkyCisla]:
		# vytvoříme jednotlivé sloupce výsledkovky
		radky_vysledkovky = []

		setrizeni_resitele_slovnik = {}
		setrizeni_resitele = m.Resitel.objects.filter(id__in=self.setrizeni_resitele_id).select_related('osoba')

		for r in setrizeni_resitele:
			setrizeni_resitele_slovnik[r.id] = r

		for i, ar_id in enumerate(self.setrizeni_resitele_id):
			if self.setrizene_body[i] > 0:
				# získáme seznam bodů za problémy pro daného řešitele
				body_problemy = []
				body_podproblemy = []
				for hp in self.temata_a_spol:
					body_problemy.append(self.body_za_temata[hp.id][ar_id])
					body_podproblemy.append([
						self.problemy_slovnik[hp.id][it.id][ar_id]
						for it in self.podproblemy[hp.id]
					])
				if self.je_nejake_ostatni:
					body_problemy.append(self.body_za_temata[-1][ar_id])
					body_podproblemy.append(
						[self.problemy_slovnik[-1][it.id][ar_id] for it in self.podproblemy[-1]])
				# vytáhneme informace pro daného řešitele
				radek = self.RadekVysledkovkyCisla(
					poradi=self.poradi[i],
					resitel=setrizeni_resitele_slovnik[ar_id],
					temata_seznamk=body_problemy,
					body_cislo=self.body_za_cislo[ar_id],
					body_rocnik=self.setrizene_body[i],
					body_odjakziva=self.resitel_body_odjakziva_slovnik[ar_id],
					rok=self.rocnik,
					body_podproblemy=body_podproblemy,  # body všech podproblémů
					body_podproblemy_iter=FixedIterator(body_podproblemy.__iter__())
				)  # ročník semináře pro zjištění ročníku řešitele
				radky_vysledkovky.append(radek)
		return radky_vysledkovky

	@staticmethod
	def pricti_body(slovnik, resitel, body):
		""" Přiřazuje danému řešiteli body do slovníku. """
		# testujeme na None (""), pokud je to první řešení
		# daného řešitele, předěláme na 0
		# (v dalším kroku přičteme reálný počet bodů),
		# rozlišujeme tím mezi 0 a neodevzdaným řešením

		if slovnik[resitel.id] == "":
			slovnik[resitel.id] = 0

		slovnik[resitel.id] += body

	@staticmethod
	def ne_clanek_ne_konfera(problem):
		inst = problem.get_real_instance()
		return not (isinstance(inst, m.Clanek) or isinstance(inst, m.Konfera))


class VysledkovkaDoTeXu(VysledkovkaCisla):
	def __init__(
			self,
			nejake_cislo: m.Cislo,
			od_vyjma: m.Deadline,
			do_vcetne: m.Deadline
	):
		super().__init__(nejake_cislo, False, do_vcetne)
		self.od_deadlinu = od_vyjma

	@cached_property
	def problemy(self) -> list[m.Problem]:
		return m.Problem.objects.filter(hodnoceni__in=m.Hodnoceni.objects.filter(
				deadline_body__deadline__gt=self.od_deadlinu.deadline,
				deadline_body__deadline__lte=self.do_deadlinu.deadline,
			)).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem')

	@property
	def hodnoceni_do_cisla(self):
		hodnoceni = m.Hodnoceni.objects.prefetch_related(
			'problem', 'reseni', 'reseni__resitele')
		if self.jen_verejne:
			hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True)
		return hodnoceni.filter(
			deadline_body__deadline__gt=self.od_deadlinu.deadline,
			deadline_body__deadline__lte=self.do_deadlinu.deadline,
			body__isnull=False,
		)