// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title Voting
 * @dev Contrat pour un système de vote décentralisé
 * @custom:dev-run-script ./scripts/deploy.js
 */
contract Voting is Ownable {

    /**
     * @dev Structure représentant un votant
     * @param isRegistered Si l'adresse est enregistrée comme électeur
     * @param hasVoted Si l'électeur a déjà voté
     * @param votedProposalId L'ID de la proposition pour laquelle l'électeur a voté
     */
    struct Voter {
        bool isRegistered;
        bool hasVoted;
        uint votedProposalId;
    }

    /**
     * @dev Structure représentant une proposition
     * @param description La description textuelle de la proposition
     * @param voteCount Le nombre de votes reçus par la proposition
     */
    struct Proposal {
        string description;
        uint voteCount;
    }

    /**
     * @dev Énumération des différentes étapes du processus de vote
     */
    enum WorkflowStatus {
        RegisteringVoters,
        ProposalsRegistrationStarted,
        ProposalsRegistrationEnded,
        VotingSessionStarted,
        VotingSessionEnded,
        VotesTallied
    }

    /**
     * @dev Événement émis lors de l'enregistrement d'un électeur
     * @param voterAddress L'adresse de l'électeur enregistré
     */
    event VoterRegistered(address voterAddress);

    /**
     * @dev Événement émis lors du changement de statut du workflow
     * @param previousStatus Le statut précédent
     * @param newStatus Le nouveau statut
     */
    event WorkflowStatusChange(WorkflowStatus previousStatus, WorkflowStatus newStatus);

    /**
     * @dev Événement émis lors de l'enregistrement d'une proposition
     * @param proposalId L'ID de la proposition enregistrée
     */
    event ProposalRegistered(uint proposalId);

    /**
     * @dev Événement émis lorsqu'un électeur vote
     * @param voter L'adresse de l'électeur
     * @param proposalId L'ID de la proposition pour laquelle il a voté
     */
    event Voted(address voter, uint proposalId);

    // Mapping des électeurs
    mapping(address => Voter) private voters;

    // Liste des propositions
    Proposal[] private proposals;

    // Statut actuel du workflow
    WorkflowStatus public workflowStatus;

    // ID de la proposition gagnante
    uint public winningProposalId;

    // Dates limites pour les différentes phases (fonctionnalité supplémentaire 1)
    uint public registrationEndTime;
    uint public votingEndTime;

    // Mapping pour la délégation de vote (fonctionnalité supplémentaire 2)
    mapping(address => address) private delegations;

    /**
     * @dev Constructeur qui initialise le contrat
     */
    constructor() Ownable() {
        workflowStatus = WorkflowStatus.RegisteringVoters;
        // Ajout d'une proposition vide à l'index 0
        proposals.push(Proposal("Genesis Proposal", 0));
    }

    /**
     * @dev Modificateur pour vérifier si l'appelant est un électeur enregistré
     */
    modifier onlyVoters() {
        require(voters[msg.sender].isRegistered, "Vous n'etes pas un electeur enregistre");
        _;
    }

    /**
     * @dev Enregistre un nouvel électeur
     * @param _voterAddress L'adresse de l'électeur à enregistrer
     */
    function registerVoter(address _voterAddress) external onlyOwner {
        require(workflowStatus == WorkflowStatus.RegisteringVoters, "La periode d'inscription des electeurs est terminee");
        require(!voters[_voterAddress].isRegistered, "L'electeur est deja enregistre");

        voters[_voterAddress].isRegistered = true;
        emit VoterRegistered(_voterAddress);
    }

    /**
     * @dev Démarre la session d'enregistrement des propositions
     * @param _durationInMinutes Durée en minutes de la période d'enregistrement
     */
    function startProposalsRegistration(uint _durationInMinutes) external onlyOwner {
        require(workflowStatus == WorkflowStatus.RegisteringVoters, "Impossible de demarrer la session d'enregistrement");

        WorkflowStatus previousStatus = workflowStatus;
        workflowStatus = WorkflowStatus.ProposalsRegistrationStarted;

        // Définir une date limite pour l'enregistrement (fonctionnalité supplémentaire)
        registrationEndTime = block.timestamp + (_durationInMinutes * 1 minutes);

        emit WorkflowStatusChange(previousStatus, workflowStatus);
    }

    /**
     * @dev Enregistre une nouvelle proposition
     * @param _description La description de la proposition
     */
    function registerProposal(string calldata _description) external onlyVoters {
        require(workflowStatus == WorkflowStatus.ProposalsRegistrationStarted, "La session d'enregistrement des propositions n'est pas active");
        require(bytes(_description).length > 0, "La description ne peut pas etre vide");
        require(block.timestamp <= registrationEndTime, "La periode d'enregistrement des propositions est terminee");

        proposals.push(Proposal(_description, 0));
        emit ProposalRegistered(proposals.length - 1);
    }

    /**
     * @dev Termine la session d'enregistrement des propositions
     */
    function endProposalsRegistration() external onlyOwner {
        require(workflowStatus == WorkflowStatus.ProposalsRegistrationStarted, "Impossible de terminer la session d'enregistrement");

        WorkflowStatus previousStatus = workflowStatus;
        workflowStatus = WorkflowStatus.ProposalsRegistrationEnded;

        emit WorkflowStatusChange(previousStatus, workflowStatus);
    }

    /**
     * @dev Démarre la session de vote
     * @param _durationInMinutes Durée en minutes de la période de vote
     */
    function startVotingSession(uint _durationInMinutes) external onlyOwner {
        require(workflowStatus == WorkflowStatus.ProposalsRegistrationEnded, "Impossible de demarrer la session de vote");

        WorkflowStatus previousStatus = workflowStatus;
        workflowStatus = WorkflowStatus.VotingSessionStarted;

        // Définir une date limite pour le vote (fonctionnalité supplémentaire)
        votingEndTime = block.timestamp + (_durationInMinutes * 1 minutes);

        emit WorkflowStatusChange(previousStatus, workflowStatus);
    }

    /**
     * @dev Soumet un vote pour une proposition
     * @param _proposalId L'ID de la proposition
     */
    function vote(uint _proposalId) external onlyVoters {
        require(workflowStatus == WorkflowStatus.VotingSessionStarted, "La session de vote n'est pas active");
        require(!voters[msg.sender].hasVoted, "Vous avez deja vote");
        require(_proposalId < proposals.length, "La proposition n'existe pas");
        require(block.timestamp <= votingEndTime, "La periode de vote est terminee");

        voters[msg.sender].hasVoted = true;
        voters[msg.sender].votedProposalId = _proposalId;

        // Si l'électeur a délégué son vote à quelqu'un, ne pas compter le vote ici
        if (delegations[msg.sender] == address(0)) {
            proposals[_proposalId].voteCount++;
        }

        emit Voted(msg.sender, _proposalId);
    }

    /**
     * @dev Délègue son vote à un autre électeur (fonctionnalité supplémentaire 2)
     * @param _to L'adresse à qui déléguer le vote
     */
    function delegateVoteTo(address _to) external onlyVoters {
        require(workflowStatus == WorkflowStatus.VotingSessionStarted, "La session de vote n'est pas active");
        require(!voters[msg.sender].hasVoted, "Vous avez deja vote");
        require(_to != msg.sender, "Vous ne pouvez pas deleguer votre vote a vous-meme");
        require(voters[_to].isRegistered, "L'adresse cible n'est pas un electeur enregistre");
        require(delegations[msg.sender] == address(0), "Vous avez deja delegue votre vote");

        address currentDelegate = _to;
        // Vérifier qu'il n'y a pas de boucle de délégation
        while (delegations[currentDelegate] != address(0)) {
            require(delegations[currentDelegate] != msg.sender, "Boucle de delegation detectee");
            currentDelegate = delegations[currentDelegate];
        }

        delegations[msg.sender] = _to;

        // Si la personne à qui on délègue a déjà voté, ajouter immédiatement le vote
        if (voters[_to].hasVoted) {
            uint proposalId = voters[_to].votedProposalId;
            proposals[proposalId].voteCount++;
            voters[msg.sender].hasVoted = true;
            voters[msg.sender].votedProposalId = proposalId;
            emit Voted(msg.sender, proposalId);
        }
    }

    /**
     * @dev Termine la session de vote
     */
    function endVotingSession() external onlyOwner {
        require(workflowStatus == WorkflowStatus.VotingSessionStarted, "Impossible de terminer la session de vote");

        WorkflowStatus previousStatus = workflowStatus;
        workflowStatus = WorkflowStatus.VotingSessionEnded;

        emit WorkflowStatusChange(previousStatus, workflowStatus);
    }

    /**
     * @dev Comptabilise les votes et détermine la proposition gagnante
     */
    function tallyVotes() external onlyOwner {
        require(workflowStatus == WorkflowStatus.VotingSessionEnded, "Le comptage des votes ne peut pas encore commencer");

        WorkflowStatus previousStatus = workflowStatus;
        workflowStatus = WorkflowStatus.VotesTallied;

        uint winningVoteCount = 0;

        for (uint i = 0; i < proposals.length; i++) {
            if (proposals[i].voteCount > winningVoteCount) {
                winningVoteCount = proposals[i].voteCount;
                winningProposalId = i;
            }
        }

        emit WorkflowStatusChange(previousStatus, workflowStatus);
    }

    /**
     * @dev Termine automatiquement les phases si les délais sont dépassés (fonctionnalité supplémentaire 1)
     */
    function checkAndUpdateStatus() external {
        if (workflowStatus == WorkflowStatus.ProposalsRegistrationStarted && block.timestamp > registrationEndTime) {
            WorkflowStatus previousStatus = workflowStatus;
            workflowStatus = WorkflowStatus.ProposalsRegistrationEnded;
            emit WorkflowStatusChange(previousStatus, workflowStatus);
        }
        else if (workflowStatus == WorkflowStatus.VotingSessionStarted && block.timestamp > votingEndTime) {
            WorkflowStatus previousStatus = workflowStatus;
            workflowStatus = WorkflowStatus.VotingSessionEnded;
            emit WorkflowStatusChange(previousStatus, workflowStatus);
        }
    }

    /**
     * @dev Récupère les informations d'un électeur
     * @param _voterAddress L'adresse de l'électeur
     * @return isRegistered Si l'électeur est enregistré
     * @return hasVoted Si l'électeur a voté
     * @return votedProposalId L'ID de la proposition pour laquelle l'électeur a voté
     */
    function getVoter(address _voterAddress) external view onlyVoters returns (bool isRegistered, bool hasVoted, uint votedProposalId) {
        Voter memory voter = voters[_voterAddress];
        return (voter.isRegistered, voter.hasVoted, voter.votedProposalId);
    }

    /**
     * @dev Récupère les informations d'une proposition
     * @param _proposalId L'ID de la proposition
     * @return description La description de la proposition
     * @return voteCount Le nombre de votes pour la proposition
     */
    function getProposal(uint _proposalId) external view returns (string memory description, uint voteCount) {
        require(_proposalId < proposals.length, "La proposition n'existe pas");
        Proposal memory proposal = proposals[_proposalId];
        return (proposal.description, proposal.voteCount);
    }

    /**
     * @dev Récupère la proposition gagnante
     * @return winningProposalIndex L'ID de la proposition gagnante
     * @return description La description de la proposition gagnante
     * @return voteCount Le nombre de votes pour la proposition gagnante
     */
    function getWinner() external view returns (uint winningProposalIndex, string memory description, uint voteCount) {
        require(workflowStatus == WorkflowStatus.VotesTallied, "Les votes n'ont pas encore ete comptabilises");
        return (winningProposalId, proposals[winningProposalId].description, proposals[winningProposalId].voteCount);
    }

    /**
     * @dev Récupère le nombre total de propositions
     * @return Le nombre de propositions
     */
    function getProposalsCount() external view returns (uint) {
        return proposals.length;
    }

    /**
     * @dev Vérifie à qui un électeur a délégué son vote (fonctionnalité supplémentaire 2)
     * @param _voterAddress L'adresse de l'électeur
     * @return L'adresse à qui le vote a été délégué, ou address(0) si pas de délégation
     */
    function getDelegation(address _voterAddress) external view onlyVoters returns (address) {
        return delegations[_voterAddress];
    }
}