# encoding=utf-8
"""
File for the CotationWindow top object, which serves as the parent class for all evaluation windows.
This class handles database interactions, UI initialization, and manages participant data storage and retrieval.
"""

__author__ = "Roland Trouville"
__copyright__ = "Copyright 2015+, Consortium MonPaGe"
__license__ = "Creative Commons 4.0 By-Nc-Sa"
__maintainer__ = "Roland Trouville"
__email__ = "contact.monpage@gmail.com"
__status__ = "Production"

import collections
import os
from sqlite3 import Row
from typing import List, Optional, Tuple

from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import QMainWindow, QWidget

from tools.db_manager import DBManager
from tools.display_tools import DisplayTools
from tools.general_tools import GeneralTools
from tools.participant_manager import ParticipantManager


class CotationWindow(QWidget):
	"""
	Parent class of all the windows used for cotation (evaluation).

	This class provides the foundation for managing speech and language assessments,
	including database operations, result storage/retrieval, and UI initialization.
	It delegates the management of the two databases (reference and participant results)
	and provides methods for setting/getting results and checking data for stimuli.

	Attributes:
		module_name (str): Name of the evaluation module
		directory_path (str): Path to the module's directory
		participant_code (str): Unique identifier for the participant
		reference_cotation_db (DBManager): Reference to the main cotation database
		participant_result_db (DBManager): Reference to the participant's results database
	"""

	module_name: str
	""" Name of the evaluation module """

	directory_path: str
	""" Path to the module's directory """

	participant_code: str
	""" Unique identifier for the participant """

	reference_cotation_db: DBManager
	""" Reference to the main cotation database """

	participant_result_db: DBManager
	""" Reference to the participant's results database """

	def __init__(self, participant_code: str, parent: QMainWindow):
		"""
		Initialize the CotationWindow.

		Args:
			participant_code: Unique identifier for the participant being evaluated
		"""
		super().__init__()
		self.parentWindow = parent
		self.participant_code = participant_code
		ParticipantManager.set_current_participant(participant_code)

	def closeEvent(self, event: QCloseEvent):
		"""
		Manages the click on the close icon of the window.

		Args:
			event: Close event triggered by the window system
		"""
		self.end_cotation()

	def end_cotation(self):
		"""
		Called when the cotation is finished or needs to be terminated.

		Shows the parent widget if it exists and closes the current window.
		"""
		self.parentWindow.show()

		self.close()

	def init_ui(self):
		"""
		Initialize the UI components of the cotation window.

		Sets the window geometry, size, and title based on configuration and module info.
		"""
		self.setGeometry(DisplayTools.get_window_display_rect())
		# self.setFixedSize(800, 600)
		self.setFixedSize(DisplayTools.get_window_size())
		self.setWindowTitle(
			f"MonPaGe Cotation - {GeneralTools.get_version()} {self.module_name} - {self.directory_path}"
		)

	def init_cotation(self):
		"""
		Opens a connection to the cotation main database.

		Raises:
			IOError: If the cotation database file cannot be found
		"""
		if os.path.isfile("./data/cotation/cotation.db"):
			self.reference_cotation_db = DBManager.get_cotation_db()
		else:
			GeneralTools.alert_box(
				self,
				"Le fichier ./data/cotation/cotation.db est manquant ou introuvable",
				None,
				True,
			)
			raise IOError(
				"Le fichier ./data/cotation/cotation.db est manquant ou introuvable"
			)

	def init_participant_result_db(self):
		"""
		Initializes the participant's result database.

		Creates a participant-specific database for storing cotation results.
		"""
		self.participant_result_db = self.reference_cotation_db.create_user_cotation_db(
			ParticipantManager.cotation_db_filename
		)

	def get_stored_result(
		self,
		session_path: str,
		participant_code: str,
		judge: str,
		pw_id: int,
		syll_pos: int,
		phon_pos: int,
	) -> Optional[Row]:
		"""
		Get the result that is stored in user database for the corresponding parameters.

		Args:
			session_path: Session path to get result for
			participant_code: Code of the participant to get result for
			judge: The code of the judge doing the evaluation
			pw_id: The ID of the pseudo word we are getting result for
			syll_pos: The syllable position for which we are getting a result
			phon_pos: The phoneme position for which we are getting a result

		Returns:
			List of tuples containing result data (state, effort, inversion, ajout,
			error_type, error_nature, is_cluster, error_verbatim)
		"""

		sql = (
			"select state, effort,inversion, ajout, error_type, error_nature, is_cluster, error_verbatim from "
			"results_pw as r "
			"where "
			"r.pseudo_word_id = ? and r.syll_pos = ? and r.phon_pos = ? and r.session_path = ? and r.participant = "
			"? "
			"and r.judge = ?"
		)
		t = (pw_id, syll_pos, phon_pos, session_path, participant_code, judge)
		return self.participant_result_db.execute(sql, t).fetchone()

	def set_stored_result(
		self,
		session_path: str,
		participant_code: str,
		judge: str,
		pw_id: int,
		syll_pos: int,
		phon_pos: int,
		is_cluster: int,
		state: int,
		effort: int,
		inversion: int,
		ajout: int,
		phonem: str = "",
	) -> bool:
		"""
		Set a result in the user database.

		Args:
			session_path: Session path to store result for
			participant_code: Code of the participant to store result for
			judge: The code of the judge doing the evaluation
			pw_id: The ID of the pseudo word we are storing result for
			syll_pos: The syllable position for which we are storing a result
			phon_pos: The phoneme position for which we are storing a result
			is_cluster: Number of elements in the cluster (0,1: not a cluster)
			state: The value of state chosen by the judge
			effort: Boolean value (0/1) to determine if there is an effort or not
			inversion: Boolean value (0/1) to determine if there is an inversion or not
			ajout: Boolean value (0/1) to determine if there is an ajout or not
			phonem: The phonem label (for historical reasons)

		Returns:
			True if the operation was successful
		"""
		# Checking if we already have a result for those parameters
		sql = (
			"Select `pseudo_word_id` from results_pw WHERE `pseudo_word_id` = ? AND `syll_pos` = ? AND "
			"`phon_pos` = ? AND `session_path` = ? AND `participant` = ? AND `judge` = ?"
		)
		t = (pw_id, syll_pos, phon_pos, session_path, participant_code, judge)
		tmp = self.participant_result_db.execute(sql, t).fetchall()

		version = GeneralTools.get_version()

		if is_cluster == 1:  # Cluster of 1 is same as no cluster so we set at 0
			is_cluster = 0
		if len(tmp) > 0:  # We already have a result -> update
			sql = (
				"UPDATE results_pw SET `state` = ?, `effort` = ?,`inversion`=?, `ajout`=?, `is_cluster` = ?, "
				"`phonem` = ?, `version` = ? "
				"WHERE `pseudo_word_id` = ?  AND `syll_pos` = ? AND  "
				"`phon_pos` = ? AND `session_path` = ? AND `participant` = ? "
				"AND `judge` = ?"
			)
			t = (
				state,
				effort,
				inversion,
				ajout,
				is_cluster,
				phonem,
				version,
				pw_id,
				syll_pos,
				phon_pos,
				session_path,
				participant_code,
				judge,
			)
			self.participant_result_db.execute(sql, t)
		else:  # New result -> insert
			sql = (
				"insert or replace into results_pw(`pseudo_word_id`,`syll_pos`,`phon_pos`,`session_path`,"
				"`participant`, `judge`, `phonem`, `version`,\
		`is_cluster`, `state`,`effort`,`inversion`,`ajout`, `error_type`, `error_nature`, `error_verbatim`) VALUES ("
				"?,?,?,?,?,?,?,?,"
				"?,?,?,"
				"?,?,0,0,NULL)"
			)
			t = (
				pw_id,
				syll_pos,
				phon_pos,
				session_path,
				participant_code,
				judge,
				phonem,
				version,
				is_cluster,
				state,
				effort,
				inversion,
				ajout,
			)
			self.participant_result_db.execute(sql, t)
		self.participant_result_db.commit()
		return True

	def set_advanced_stored_result(
		self,
		session_path: str,
		participant_code: str,
		judge: str,
		pw_id: int,
		syll_pos: int,
		phon_pos: int,
		error_type: int = 0,
		error_nature: int = 0,
		error_verbatim: Optional[str] = None,
	) -> bool:
		"""
		For advanced result storing with additional error information.

		This method extends the basic result storage with error type, nature and verbatim details.

		Args:
			session_path: Session path to store result for
			participant_code: Code of the participant to store result for
			judge: The code of the judge doing the evaluation
			pw_id: The ID of the pseudo word we are storing result for
			syll_pos: The syllable position for which we are storing a result
			phon_pos: The phoneme position for which we are storing a result
			error_type: Value of the error type chosen by the judge
			error_nature: Value of the error nature chosen by the judge
			error_verbatim: Text description of the error, if applicable

		Returns:
			True if the operation was successful
		"""
		sql = (
			"UPDATE results_pw SET `error_type` = ?, `error_nature` = ?, `error_verbatim` = ? WHERE  "
			"`pseudo_word_id` = ? "
			"AND `syll_pos` = ? AND `phon_pos` = ? AND `session_path` = ? AND `participant` = ? AND `judge` = ?"
		)
		t = (
			error_type,
			error_nature,
			error_verbatim,
			pw_id,
			syll_pos,
			phon_pos,
			session_path,
			participant_code,
			judge,
		)
		self.participant_result_db.execute(sql, t)
		self.participant_result_db.commit()
		return True

	def get_pseudo_word_structure(self, filename: str) -> List[Tuple]:
		"""
		Get the structure of a given pseudo word.

		Retrieves detailed structural information about a pseudo word including syllables,
		phonemes, and their positions.

		Args:
			filename: The filename corresponding to the pseudo word we want the structure of

		Returns:
			List of tuples containing structural data about the pseudo word
		"""
		tmp = filename.split("_")
		tmp2 = tmp[-1].split("-")
		cat = tmp2[0]
		del tmp2[0]
		wanted = "-".join(tmp2)
		sql = """select pw.id, pw.type, pw.ortho, pws.position as syll_pos, sp.position as phon_pos, p.consonnant,
              p.api, tp.label, tp.schema, pw.filename from pseudo_word as pw
            inner join pseudo_word_syllable as pws on pws.pseudo_word_id = pw.id
            inner join syllable as s on pws.syllable_id = s.id
            inner join syllable_phonem as sp on sp.syllable_id = s.id
            inner join phonem as p on sp.phonem_id = p.id
            inner join type_desc as tp on tp.type = pw.type
            left outer join pseudo_word_phonem_special as pwps on pwps.pseudo_word_id = pw.id
            AND pwps.syllable_pos = pws.position AND pwps.phonem_pos = sp.position
            where pw.filename = ? and pw.type = ? and (pwps.ignore IS NULL or pwps.ignore = 0)
            order by pws.position ASC, sp.position ASC"""
		t = (wanted, cat)
		return self.participant_result_db.execute(sql, t).fetchall()

	def get_available_results(
		self,
		session_path: str,
		participant_code: str,
		judge: str,
		module_name: str = "ModulePseudoMots",
	) -> List[Tuple]:
		"""
		Get a list of pseudo words/stimuli for which we already have data stored.

		Retrieves all items that have been previously evaluated for the given parameters.

		Args:
			session_path: Session path to query results for
			participant_code: Code of the participant to query results for
			judge: The code of the judge who performed the evaluation
			module_name: The name of the module we want the data for

		Returns:
			An array of pseudo words IDs or stimuli filenames we have results for
		"""
		if module_name == "ModulePseudoMots":
			sql = "select DISTINCT pw.id, pw.filename, pw.type from pseudo_word as pw \
                inner join results_pw as r on r.pseudo_word_id = pw.id \
                where r.session_path = ? and r.participant = ? and r.judge = ?\
                order by pw.type ASC, pw.filename ASC"
			t = (session_path, participant_code, judge)
			return self.participant_result_db.execute(sql, t).fetchall()
		elif module_name in ("ModulePhrases", "ModuleTexte"):
			sql = (
				"select DISTINCT s.file from results_qa as r "
				"inner join stimuli as s on r.stimuli_id = s.id "
				"where r.session_path = ? and r.participant = ? and r.judge = ? and s.module= ?"
			)
			t = (session_path, participant_code, judge, module_name)
			return self.participant_result_db.execute(sql, t).fetchall()

		return []

	def get_questions_for_stimuli(
		self, module_name: str, stimuli_name: str
	) -> collections.OrderedDict:
		"""
		Get all questions to ask a judge for a given stimuli.

		Retrieves all questions associated with a specific stimulus, including
		their possible answers and metadata.

		Args:
			module_name: The module the stimuli is a part of
			stimuli_name: The name of the stimuli we want questions for

		Returns:
			An ordered dictionary containing data for all questions, with question IDs as keys
		"""
		sql = (
			"select s.`module`, s.`file`, sq.`stimuli_id`, sq.`question_id`, a.id as answer_id, a.`value`, "
			"a.label as alabel, q.label as qlabel, q.multichoice, qt.id as sub_question_id, "
			"q.tooltip, q.top_question_id from stimuli as s "
			"inner join stimuli_question as sq on sq.stimuli_id = s.id and sq.active = 1 "
			"inner join question as q on q.id = sq.question_id "
			"inner join answer as a on a.question_id = q.id "
			"left join question as qt on qt.top_question_id = q.id "
			"where s.`file` = ? and s.`module` = ?"
			+ " order by q.`order`, a.`order` ASC"
		)
		tmp = self.participant_result_db.execute(
			sql, (stimuli_name, module_name)
		).fetchall()
		ret = {}
		for t in tmp:
			if t[3] not in ret:
				ret[t[3]] = {
					"question_id": t[3],
					"stimuli_id": t[2],
					"label": t[7],
					"multichoice": t[8],
					"sub_question_id": t[9],
					"answers": [t[4:7]],
					"tooltip": t[10],
					"top_question_id": t[11],
				}
			else:
				ret[t[3]]["answers"].append(t[4:7])

		# We return the map after ordering it by keys
		return collections.OrderedDict(sorted(ret.items()))

	def get_stimuli_with_questions(self, module_name: str) -> List[str]:
		"""
		Return a list of stimuli for a given module that have questions linked to them.

		Args:
			module_name: The module name to search for stimuli

		Returns:
			A list of filenames for stimuli that have associated questions
		"""
		sql = (
			"select file from stimuli as s inner join stimuli_question as sq on sq.stimuli_id = s.id and sq.active = 1 "
			"inner join question as q on q.id = sq.question_id where s.`module` = ? "
		)
		tmp = self.reference_cotation_db.execute(sql, (module_name,)).fetchall()
		ret = []
		for t in tmp:
			ret.append(t[0])

		return ret

	def get_pseudo_words_with_structure(self) -> List[str]:
		"""
		Return a list of type and filename for pseudo words that have a defined structure.

		Returns:
			A list of strings, each containing a concatenated type and filename (format: "type-filename")
		"""
		sql = (
			"select DISTINCT type, filename from pseudo_word as pw inner join pseudo_word_syllable as pws on "
			"pw.id = pws.pseudo_word_id"
		)
		tmp = self.reference_cotation_db.execute(sql).fetchall()
		ret = []
		for t in tmp:
			ret.append(t[0] + "-" + t[1])

		return ret

	def get_stored_answer(
		self,
		session_path: str,
		participant_code: str,
		judge: str,
		stimuli_id: int,
		question_id: int,
	) -> Optional[str]:
		"""
		Get the answer stored for a question in the user database.

		Args:
			session_path: Session path to query answer for
			participant_code: Code of the participant to query answer for
			judge: The code of the judge who performed the evaluation
			stimuli_id: The ID of the stimuli
			question_id: The ID of the question

		Returns:
			The stored answer value or None if no answer is found
		"""
		sql = (
			"select value from results_qa as r where r.stimuli_id = ? and r.question_id = ? and r.session_path = ? "
			"and r.participant = ? and r.judge = ?"
		)
		t = (stimuli_id, question_id, session_path, participant_code, judge)
		val = self.participant_result_db.execute(sql, t).fetchone()
		if val is not None:
			return val[0]
		return val

	def set_stored_answer(
		self,
		session_path: str,
		participant_code: str,
		judge: str,
		stimuli_id: int,
		question_id: int,
		value: str,
	) -> bool:
		"""
		Store the answer for a question and stimuli in the database.

		Args:
			session_path: Session path to store the answer for
			participant_code: Code of the participant to store the answer for
			judge: The code of the judge performing the evaluation
			stimuli_id: The ID of the stimuli
			question_id: The ID of the question
			value: The value of the answer

		Returns:
			True if the operation was successful
		"""

		sql = (
			"Select `question_id` from results_qa as r where r.stimuli_id = ? and r.question_id = ? and "
			"r.session_path = ? and r.participant = ? and r.judge = ?"
		)
		t = (stimuli_id, question_id, session_path, participant_code, judge)
		tmp = self.participant_result_db.execute(sql, t).fetchall()
		version = GeneralTools.get_version()
		if len(tmp) > 0:  # Already have an answer -> update
			sql = (
				"UPDATE results_qa SET `value` = ?, `version`=? WHERE  `stimuli_id` = ? AND `question_id` = ? "
				"AND `session_path` = ? AND `participant` = ? AND `judge` = ?"
			)
			t = (
				value,
				version,
				stimuli_id,
				question_id,
				session_path,
				participant_code,
				judge,
			)
			self.participant_result_db.execute(sql, t)
		else:  # No answer -> insert
			sql = (
				"insert or replace into results_qa(`stimuli_id`,`question_id`,`session_path`,`participant`, `judge`,\
            `value`, `version`) VALUES (?,?,?,?,?,?, ?)"
			)
			t = (
				stimuli_id,
				question_id,
				session_path,
				participant_code,
				judge,
				value,
				version,
			)
			self.participant_result_db.execute(sql, t)
		self.participant_result_db.commit()
		return True

	def get_stored_score(
		self, session_path: str, participant_code: str, judge: str, word: str
	) -> Optional[int]:
		"""
		Get the score stored for a word in the user database.

		Args:
			session_path: Session path to query score for
			participant_code: Code of the participant to query score for
			judge: The code of the judge who performed the evaluation
			word: The word we want the score of

		Returns:
			The stored score value or None if no score is found
		"""
		sql = (
			"select score from results_int as r where r.word = ? and r.session_path = ? "
			"and r.participant = ? and r.judge = ?"
		)
		t = (word, session_path, participant_code, judge)
		val = self.participant_result_db.execute(sql, t).fetchone()
		if val is not None:
			return val[0]
		return val

	def set_stored_score(
		self,
		session_path: str,
		participant_code: str,
		judge: str,
		word: str,
		score: int,
		word_order: int = 0,
	) -> bool:
		"""
		Store the score for a word in the database.

		Args:
			session_path: Session path to store the score for
			participant_code: Code of the participant to store the score for
			judge: The code of the judge performing the evaluation
			word: The word we want to store the score for
			score: The score value for the word
			word_order: Optional ordering value for the word (default=0)

		Returns:
			True if the operation was successful
		"""

		sql = (
			"Select `word` from results_int as r where r.word = ? and "
			"r.session_path = ? and r.participant = ? and r.judge = ?"
		)
		t = (word, session_path, participant_code, judge)
		version = GeneralTools.get_version()
		tmp = self.participant_result_db.execute(sql, t).fetchall()
		if len(tmp) > 0:  # Already have an answer -> update
			sql = (
				"UPDATE results_int SET `score` = ?, `version` = ?, `word_order` = ? WHERE  `word` = ? "
				"AND `session_path` = ? AND `participant` = ? AND `judge` = ?"
			)
			t = (
				score,
				version,
				word_order,
				word,
				session_path,
				participant_code,
				judge,
			)
			self.participant_result_db.execute(sql, t)
		else:  # No answer -> insert
			sql = (
				"insert or replace into results_int(`word`,`session_path`,`participant`, `judge`,\
            `score`, `version`,`word_order`) VALUES (?,?,?,?,?, ?, ?)"
			)
			t = (
				word,
				session_path,
				participant_code,
				judge,
				score,
				version,
				word_order,
			)
			self.participant_result_db.execute(sql, t)
		self.participant_result_db.commit()
		return True
