import random
from typing import Dict, List, Optional
from dataclasses import dataclass
import sys
from logger import log
from finger_count import *
from playsound import playsound
import wave
from piper import PiperVoice
import os
from art import *
from translation import *


@dataclass
class Player:
	name: str
	alias: str
	role: str
	alive: bool = True
	protected: bool = False


class Game:
	def __init__(self):
		# single source of truth: name -> Player
		self.players: Dict[str, Player] = {}
		
		# lovers
		self.lovers: Optional[Tuple[str, str]] = None

		# Dead this night
		self.dead_this_night: [List[str]] = []

		# Used potions
		self.used_potions: [list[str]] = []
		# Language
		self.language: str = "fr"
	# -------------------------
	# setup / I/O
	# -------------------------
	def load_lists(self) -> Optional[Dict[str, List[str]]]:
		"""
		Load players and roles from files and return them as lists.
		Returns dict with keys 'players' and 'roles'.
		"""
		players_file = "players.txt"
		roles_file = "roles.txt"
		words_file = "randoms_words.txt"
		log.info("Loading lists...")

		try:
			with open(players_file, "r", encoding="utf-8") as pf:
				player_names = [line.strip() for line in pf if line.strip()]
		except FileNotFoundError:
			log.critical("Players file not found: %s", players_file)
			return None

		try:
			with open(roles_file, "r", encoding="utf-8") as rf:
				roles_list = [line.strip() for line in rf if line.strip()]
		except FileNotFoundError:
			log.critical("Roles file not found: %s", roles_file)
			return None

		try:
			with open(words_file, "r", encoding="utf-8") as rf:
				alias_list = [line.strip() for line in rf if line.strip()]
		except FileNotFoundError:
			log.critical("Roles file not found: %s", words_file)
			return None
	

		log.info("Load complete!")

		return {"players": player_names, "roles": roles_list, "alias": alias_list}

	# -------------------------
	# player / role assignment
	# -------------------------
	def give_roles_to_players(self, player_names: Optional[List[str]] = None,
							  roles_list: Optional[List[str]] = None,
							  alias_list: Optional[List[str]] = None) -> None:
		"""
		Assign a random role to each player. Accepts optional lists (returned by load_lists).
		If lists are not supplied, it will attempt to read files itself.
		"""
		number = 0		
		if not player_names:
			log.info("Players are not initialized")
			return
		if not roles_list:
			log.info("Roles are not initialized")
			return
		if len(roles_list) < len(player_names):
			log.error("Not enough roles for players (roles: %d, players: %d)",
					  len(roles_list), len(player_names))
			return
		if len(alias_list) < len(player_names):
			log.error("Not enough randoms words for players (words: %d, players: %d)",
					  len(alias_list), len(player_names))
			return

		available_roles = list(roles_list)
		random.shuffle(available_roles)
		available_alias = list(alias_list)
		random.shuffle(alias_list)

		# clear any existing players (safe to reassign)
		self.players.clear()
		# clear any player name audio
		if not os.path.exists("./voicesline/playersname"):
			    os.makedirs("./voicesline/playersname")
		for filename in os.listdir("./voicesline/playersname"):
			file_path = os.path.join("./voicesline/playersname", filename)
   			 # Check if it is a file (not a subdirectory)
			if os.path.isfile(file_path):
				os.remove(file_path)  # Remove the file	
		for filename in os.listdir("./voicesline/alias"):
			file_path = os.path.join("./voicesline/alias", filename)
   			 # Check if it is a file (not a subdirectory)
			if os.path.isfile(file_path):
				os.remove(file_path)  # Remove the file
		if self.language == "fr":
			voice = PiperVoice.load("./voices/fr/fr_FR-tom-medium.onnx")
		elif self.language == "en":
			voice = PiperVoice.load("./voices/en/en_GB-alan-medium.onnx")
		else:
			log.info("Unknow language")
		for name in player_names:
			chosen_role = available_roles.pop()
			chosen_alias = available_alias.pop()
			with wave.open(f"./voicesline/playersname/{name}.wav", "wb") as wav_file:
				    voice.synthesize_wav(name, wav_file)
			with wave.open(f"./voicesline/alias/{chosen_alias}.wav", "wb") as wav_file:
				    voice.synthesize_wav(chosen_alias, wav_file)
			number += 1
			self.players[number] = Player(name=name, role=chosen_role, alias=chosen_alias)
			self.play_sound("players", name.capitalize())
			self.play_sound("prompts", "first_wakes")
			if name == "Dan":
				self.play_sound("prompts", "cheater")
			self.show_text(chosen_role)
			self.show_text(chosen_alias)
			time.sleep(3)
			os.system('clear')
			self.play_sound("players", name.capitalize())
			self.play_sound("prompts", "sleep_again")




		log.debug("Assigned roles:")
		for player in self.players.values():
			log.debug("%s: %s", player.name, player.role)

	# -------------------------
	# utilities
	# -------------------------
	def clear_data(self) -> None:
		for player in self.players:
			self.players[player].protected = False


	def couple(self) -> None:
		os.system('clear')
		self.play_sound("prompts", "lovers")
		log.debug(self.lovers)
		for player in self.lovers:
			log.info(self.players[player].alias)
			self.play_sound("alias", self.players[player].alias)
			self.show_text(self.players[player].role)
		time.sleep(10)
		
	def show_text(self, text: str) -> None:
		if self.language == "fr":
			if text in FR:
				Art=text2art(FR.get(text))
				print(Art)
			else:
				Art=text2art(text)
				print(Art)
		if self.language == "en":
				Art=text2art(text)
				print(Art)

	def get_remaining_roles(self) -> List[str]:
		remaining_roles = []
		for player in self.players:
			if self.players[player].alive:
				remaining_roles.append(self.players[player].role)
		log.DEBUG(remaining_roles)
		return remaining_roles

	def number_2_name(self, player_number: int):
		player_name = self.players[player_number].name
		return player_name

	def play_sound(self, type, soundinfo) -> None:
		if type == "prompts":
			f = f"./voicesline/prompts/{self.language}/{soundinfo}.wav"
		elif type == "roles":
			f = f"./voicesline/roles/{self.language}/{soundinfo}.wav"
		elif type == "players":
			f = f"./voicesline/playersname/{soundinfo}.wav"
		elif type == "alias":
			f = f"./voicesline/alias/{soundinfo}.wav"
		else:
			log.debug("An error occurred")
			return
		time.sleep(0.2) 
		playsound(f)

	def select_someone(self, prompt: Optional[str] = None) -> Optional[str]:
		"""
		Prompt the user to enter a player name until a valid one is entered.
		Returns None on EOF/KeyboardInterrupt.
		"""
		prompt = prompt or "show_number"
		#self.play_sound("prompts", prompt)
		while True:
			selected = count_fingers()

			# exact match
			if selected in self.players:
				return selected
			
	def choose_between(self, options):
		#Prompt the user to choose between options until a valid one is entered
		numbered_options = " ".join([f"{i+1}: {option}" for i, option in enumerate(options)])
		log.info(f"Choose between {numbered_options}:")  
		while True:
			selected = count_fingers()
			try:
				index = int(selected) - 1
				if 0 <= index < len(options):
					chosen_option = options[index]
					log.debug("You chose %s!", chosen_option)
					return chosen_option
				else:
					log.warning("Invalid number entered.")
			except ValueError:
				log.warning("Invalid input. Please enter an option or number.")
	# -------------------------
	# game actions
	# -------------------------
	def put_in_love(self, player1: str, player2: str) -> None:
		"""Link two players such that if one dies the other dies too."""
		if player1 not in self.players or player2 not in self.players:
			log.error("One or both players do not exist: %s, %s", player1, player2)
			return

		self.lovers = (player1, player2)

		log.debug("Players now put in love: %s <-> %s", self.players[player1], self.players[player2])

		log.info("They are now bounded by love.")
		log.info("They wake up and reveal their role to each other.")
		log.info("Those two go back to sleep.")


	def kill(self, player: str) -> None:
		"""Kill a player."""
		if player not in self.players:
			log.error("Couldn't kill unknown player: %s", player)
			return

		target = self.players[player]

		# already dead?
		if not target.alive:
			log.error("%s is already dead!", player)
			return

		# protected?
		if target.protected:
			log.debug("%s was protected and survives.", player)
			return

		# in love?
		if player in self.lovers:
			log.debug("%s is in love! Killing them and their lover.", player)
			for p in self.lovers:
				# kill them and their lover
				log.debug("Killed %s", p)
				self.dead_this_night.append(p)
				self.players[p].alive = False
			return

		# else just kill them
		log.info("Killed %s" % player)
		self.dead_this_night.append(player)
		target.alive = False

	def revive(self, player: str) -> None:
		"""Revive a player."""
		log.info("Players that will die this night are: %s", self.dead_this_night)
		if player not in self.players:
			log.error("Tried to revive unknown player: %s", player)
			return
		p = self.players[player]
		if not p.alive:
			p.alive = True
			log.info("%s has been revived.", player)
		else:
			log.info("%s is already alive.", player)

	# -------------------------
	# helpers
	# -------------------------
	def status(self) -> None:
		"""Log current players' statuses for debugging."""

		# player values
		for p in self.players.values():
			log.debug("%s -> role: %s, alive: %s, Protected: %s",
					 p.name, p.role, p.alive, p.protected)
		# lovers
		log.debug(f"Lovers: {self.lovers}")

	def role(func):
		"""Decorator for roles"""
		def wrapper(self, *args, **kwargs):
			# the role is the name of the function thats decorated
			role = func.__name__.capitalize()
			os.system('clear')
			if role in self.get_remaining_roles():
				self.play_sound("roles", role)
				self.play_sound("prompts", "wakes")
				func(self, *args, **kwargs)
				self.play_sound("roles", role)
				self.play_sound("prompts", "sleep_again")
				log.INFO("%s goes back to sleep." % role)		
				self.status()

		return wrapper

	# -------------------------
	# roles
	# -------------------------

	@role
	def cupidon(self) -> None:
		"""Interactively pick two players to link by love."""
		self.play_sound("prompts", "linked_death")
		self.play_sound("prompts", "first_bound")
		p1 = self.select_someone()
		if p1 is None:
			return
		self.play_sound("prompts", "second_bound")
		p2 = self.select_someone()
		if p2 is None:
			return
		if p1 == p2:
			log.info("Cannot link a player to themselves.")
			return
		self.put_in_love(p1, p2)

	@role
	def savior(self) -> None:
		"""Interactively choose someone to protect."""
		self.play_sound("prompts", "choose_protect")
		protected = self.select_someone()
		if protected is None:
			return			
		self.players[protected].protected = True
		log.debug("Protected: %s", protected)

	@role
	def witch(self) -> None:
		"""Interactively choose to kill or revive someone"""
		self.play_sound("prompts", "potions_choice")
		self.play_sound("prompts", "might_die")
		for player in self.dead_this_night:
			self.show_text(self.players[player].name)
		if len(self.used_potions) >= 2:
			self.show_text("Vous avez utilisé toutes vos potions. ")
			time.sleep(5)
		else:
			while True:
				options = ("Death", "Revive", "Nothing")
				potionchoice = self.choose_between(options).capitalize()
				if potionchoice == "Revive" and "Revive" not in self.used_potions:
					player = self.select_someone()
					if player in self.dead_this_night:
						self.used_potions.append("Revive")
						self.revive(player)
						return
					elif player not in self.dead_this_night and not self.players[player].alive: 
						self.show_text("Tu ne peux pas faire revivre cette personne.")
					elif self.players[player].alive:
						self.show_text("Cette personne n'est pas morte.")
					else:
						log.info("Unknown error: Invalid player choice")
						return
				elif potionchoice == "Death" and "Death" not in self.used_potions:
					player = self.select_someone()
					if self.players[player].alive:
						self.used_potions.append("Death")
						self.kill(player)
						time.sleep(1)
						return
					elif not self.players[player].alive:
						self.show_text("Ce joueur est déjà mort.")
					else:
						log.info("Unknown error: Invalid player choice")
						return
				elif potionchoice == "Nothing":
					self.show_text("Vous ne faites rien cette nuit.")
					return
				elif potionchoice in self.used_potions:
					log.info("You already used this potion.")
				else:
					log.critical("Unknown error: Invalid potion choice.")
					return
	@role
	def werewolf(self) -> None:
		self.play_sound("prompts", "werewolf_choice")
		player = self.select_someone()
		self.kill(player)
	
	@role
	def seer(self) -> None:
		log.info("Choose a player to discover their role.")
		while True:
			player = self.select_someone()
			if "Seer" == self.players[player].role:
				self.show_text("You can't see your own role.")
			else:
				self.show_text(self.players[player].role)
				time.sleep(3)
				return

	@role
	def hunter(self) -> None:
		self.play_sound("prompts", "hunter")
		player = self.select_someone()
		self.kill(player)
		
	# -------------------------
	# game flow
	# -------------------------
	def first_night_cycle(self) -> None:
		self.play_sound("prompts", "fall_asleep_all")
		self.cupidon()
		self.couple()
		self.savior()
		self.werewolf()
		self.witch()
		self.seer()
		self.day()

	def night(self) -> None:
		self.dead_this_night =  []
		self.play_sound("prompts", "fall_asleep_all")
		self.savior()
		self.werewolf()
		self.witch()
		self.seer()
		self.day()
			
	def day(self) -> None:
		self.play_sound("prompts", "wake_up_all")
		self.play_sound("prompts", "night_deaths")
		for player in self.dead_this_night:
			self.play_sound("players", self.number_2_name(player))
		for player in self.dead_this_night:
			if self.players[player].role == "Hunter":
				self.hunter()
		self.play_sound("prompts", "village_vote")
		while True:
			if count_fingers(stable_duration=4) == 6:
				break
		self.play_sound("prompts", "now_show_number")
		player = self.select_someone()
		self.kill(player)
		self.night()



if __name__ == "__main__":
	print("---\nWerewolfGame\n---")

	# instantiate the game
	game = Game()

	# I/O: read files and assign roles
	loaded = game.load_lists()
	try:
		if loaded:
			# start the game!
			game.give_roles_to_players(loaded["players"], loaded["roles"], loaded["alias"])
			game.first_night_cycle()
	
	# CTRL+C		
	except KeyboardInterrupt:
		print() # new line
		log.info("Bye bye!")
		sys.exit(0)
	
	# Any unhandled exception
	except Exception as e:
		log.exception("Unhandled exception: %s" % e)
		sys.exit(1)
		
