import base64
import random
import csv
import math
import os
import string

import requests
import time

from eolymp.judge import contest_pb2, Submission, SetPasscodeInput, PasscodeServiceClient, RemovePasscodeInput, \
    ResetPasscodeInput
from eolymp.judge.contest_pb2 import Contest
from eolymp.judge.contest_service_pb2 import DescribeContestInput, UpdateContestInput, DeleteContestInput, \
    CreateContestInput, ListContestsInput
from eolymp.judge.judge_http import JudgeClient
from eolymp.judge.participant_service_pb2 import DescribeParticipantInput, ListParticipantsInput, AssignParticipantInput
from eolymp.judge.problem_service_pb2 import ListProblemsInput, ImportProblemInput, SyncProblemInput, \
    DescribeProblemInput
from eolymp.judge.submission_service_pb2 import ListSubmissionsInput, RetestProblemInput
from eolymp.ranker.ranker_pb2 import ListScoreboardsInput, ListScoreboardRowsInput
from eolymp.wellknown import expression_pb2
from google.protobuf import timestamp_pb2


from peolymp.abstract import AbstractAPI
from peolymp.utils import langext


class JudgeAPI(AbstractAPI):

    def __init__(self, space, contest_id=None):
        super(JudgeAPI, self).__init__(space)
        self.client = JudgeClient(self.http_client, url=self.get_url())
        if contest_id is not None:
            self.contest = self.client.DescribeContest(DescribeContestInput(contest_id=contest_id)).contest
            self.contest_id = contest_id
            self.password_client = PasscodeServiceClient(self.http_client,
                                                         url=self.get_url() + "/contests/" + contest_id)

    def describe_problem(self, contest_id, problem_id):
        return self.client.DescribeProblem(DescribeProblemInput(contest_id=contest_id, problem_id=problem_id)).problem

    def update_contest_start_end_time(self, contest_id, start_time, end_time):
        return self.client.UpdateContest(UpdateContestInput(contest_id=contest_id, contest=Contest(
            starts_at=timestamp_pb2.Timestamp(seconds=start_time), ends_at=timestamp_pb2.Timestamp(seconds=end_time)),
                                                            patch=[UpdateContestInput.STARTS_AT,
                                                                   UpdateContestInput.ENDS_AT]))

    def update_contest_upsolve(self, contest_id, free_upsolve, virtual_upsolve):
        print(contest_id)
        return self.client.UpdateContest(UpdateContestInput(contest_id=contest_id,
                                                            contest=Contest(
                                                                upsolve=Contest.Upsolve(
                                                                    free_upsolve=free_upsolve,
                                                                    virtual_upsolve=virtual_upsolve
                                                                )
                                                            ),
                                                            patch=[UpdateContestInput.UPSOLVE]
                                                            ))

    def update_contest_logo(self, contest_id, logo_link):
        return self.client.UpdateContest(UpdateContestInput(contest_id=contest_id, contest=Contest(logo_url=logo_link,
                                                                                                   appearance=Contest.Appearance(
                                                                                                       logo_url=logo_link)),
                                                            patch=[UpdateContestInput.LOGO_URL,
                                                                   UpdateContestInput.APPEARANCE]))

    def make_contest_scoreboard_public(self, contest_id):
        return self.client.UpdateContest(UpdateContestInput(contest_id=contest_id, contest=Contest(
            scoreboard=Contest.Scoreboard(visibility=Contest.Scoreboard.PUBLIC)),
                                                            patch=[UpdateContestInput.SCOREBOARD]))

    def convert_scale_to_eolymp_scale(self, scale):
        scales = {
            "local": Contest.Taxonomy.LOCAL,
            "regional": Contest.Taxonomy.REGIONAL,
            "national": Contest.Taxonomy.NATIONAL,
            "international": Contest.Taxonomy.INTERNATIONAL
        }
        return scales[scale]

    def update_contest_taxomany(self, contest_id, year, series, scale):
        return self.client.UpdateContest(UpdateContestInput(contest_id=contest_id, contest=Contest(
            taxonomy=Contest.Taxonomy(year=year, series=series, scale=self.convert_scale_to_eolymp_scale(scale),
                                      country="ua")), patch=[UpdateContestInput.TAXONOMY]))

    def get_contest_scoreboard_public_id(self):
        return Contest.Scoreboard.PUBLIC

    def add_participant(self, member_id, contest_id=None):
        if contest_id is None:
            contest_id = self.contest_id
        return self.client.AssignParticipant(
            AssignParticipantInput(contest_id=contest_id, member_id=member_id, inactive=False)).participant_id

    def add_problem(self, problem_id, index, submit_limit=50, score_by_best_testset=True, contest_id=None):
        if contest_id is None:
            contest_id = self.contest_id
        return self.client.ImportProblem(
            ImportProblemInput(contest_id=contest_id, import_id=str(problem_id), index=index,
                               submit_limit=submit_limit, score_by_best_testset=score_by_best_testset))

    def sync_problem(self, problem_id, contest_id):
        if contest_id is None:
            contest_id = self.contest_id
        return self.client.SyncProblem(SyncProblemInput(problem_id=problem_id, contest_id=contest_id))

    def sync_all_problems(self, contest_id, problem_number=-1):
        problems = self.get_problems(contest_id)
        counter = 0
        for problem in problems:
            if problem_number == -1 or counter == problem_number:
                res = self.sync_problem(problem.id, contest_id)
                print(problem.index, res)
                time.sleep(1)
            counter += 1

    def unlock_all_participants(self, contest_id=None):
        if contest_id is None:
            contest_id = self.contest_id
        for participant in self.get_participants(contest_id):
            if participant.passcode != '':
                print(participant.id)
                self.password_client.RemovePasscode(
                    RemovePasscodeInput(contest_id=contest_id, participant_id=participant.id))

    def lock_participant_by_id(self, participant_id, contest_id=None, passcode=None):
        if contest_id is None:
            contest_id = self.contest_id
        if passcode:
            self.password_client.SetPasscode(
                SetPasscodeInput(participant_id=participant_id, contest_id=contest_id, passcode=str(passcode))
            )
            return passcode
        else:
            out = self.password_client.ResetPasscode(
                ResetPasscodeInput(participant_id=participant_id, contest_id=contest_id))
            return out.passcode

    def get_scoreboards(self):
        return self.client.ListScoreboards(ListScoreboardsInput(contest_id=self.contest_id))

    def describe_contest(self):
        return self.client.DescribeContest(DescribeContestInput(contest_id=self.contest_id)).contest

    def delete_contest(self, contest_id=None):
        if contest_id is None:
            contest_id = self.contest_id
        self.client.DeleteContest(DeleteContestInput(contest_id=contest_id))

    def delete_all_contests(self):
        for contest in self.get_contests():
            self.delete_contest(contest.id)

    def get_problems(self, contest_id=None):
        if contest_id is None:
            contest_id = self.contest_id
        return self.client.ListProblems(ListProblemsInput(contest_id=contest_id)).items

    def get_default_scoreboard(self):
        assert False


    def create_contest(self, name, start, end, logo_link="", domain="", virtual=False, duration=0):
        if domain == "":
            letters = string.ascii_lowercase
            domain = ''.join(random.choice(letters) for i in range(20))
        contest = contest_pb2.Contest(
            name=name,
            starts_at=timestamp_pb2.Timestamp(seconds=start),
            ends_at=timestamp_pb2.Timestamp(seconds=end),
            status=contest_pb2.Contest.Status.SCHEDULED,
            visibility=contest_pb2.Contest.Visibility.PRIVATE,
            participation_mode=contest_pb2.Contest.ParticipationMode.VIRTUAL if virtual else
            contest_pb2.Contest.ParticipationMode.ONLINE,
            format=contest_pb2.Contest.Format.IOI,
            appearance=contest_pb2.Contest.Appearance(
                logo_url=logo_link
            ),
            duration=duration
        )
        res = self.client.CreateContest(CreateContestInput(contest=contest))
        print(res)
        return res.contest_id

    def get_contest(self, contest_id=None):
        if contest_id is None:
            contest_id = self.contest_id
        return self.client.DescribeContest(DescribeContestInput(contest_id=contest_id)).contest

    def get_contests(self):
        def __get_contests(offset, size):
            return self.client.ListContests(request=ListContestsInput(offset=offset, size=size))

        return self.get_many(__get_contests)

    def get_scoring(self, contest_id=None):
        if contest_id is None:
            contest_id = self.contest_id
        return self.client.DescribeContest(DescribeContestInput(contest_id=contest_id)).contest.scoring

    def allow_upsolve(self, contest_id=None):
        self.update_contest_upsolve(contest_id, True, True)

    def get_participant(self, participant_id):

        print(self.contest_id, participant_id)
        return self.client.DescribeParticipant(
            DescribeParticipantInput(contest_id=self.contest_id, participant_id=participant_id)).participant

    def get_participants(self, contest_id):
        def __get_pages(offset, size):
            return self.client.ListParticipants(
                request=ListParticipantsInput(contest_id=contest_id, offset=offset, size=size))

        return self.get_many(__get_pages)

    def find_all_submissions(self):
        STEP = 100
        rep = []
        while True:
            time.sleep(0.5)
            result = self.client.ListSubmissions(
                ListSubmissionsInput(extra=[1], contest_id=self.contest_id, size=STEP, offset=len(rep)))
            new = 0
            for item in result.items:
                new += 1
                rep += [item]
            print(new)
            if new != STEP:
                break
        return rep

    def rejudge_problem(self, problem_id):
        self.client.RetestProblem(RetestProblemInput(contest_id=self.contest_id, problem_id=problem_id))


    def get_table_languages(self, contest_id, dest):
        contest = self.describe_contest(contest_id)
        data = {}
        submissions = self.find_all_submissions(contest_id)
        for submission in submissions:
            print(submission)
            language = submission.lang
            if language not in data:
                data[language] = [0, 0]
            score = int(float(submission.score))
            if score == 100:
                data[language][0] += 1
            else:
                data[language][1] += 1

        table = []
        for language in data:
            table += [[language] + data[language]]

        print(table)

    #  xlsx.save(dest, [20, 6, 6], table)
    # print(table)

    def download_sources(self, dest, only_official=True, store_data=True):

        print("Downloading source codes for", self.contest_id)

        contest = self.get_contest()
        ends_at = contest.ends_at.seconds

        # get problems
        print(self.contest_id)
        out = self.client.ListProblems(ListProblemsInput(contest_id=self.contest_id))
        print(out.total)
        problems = dict({})
        for item in out.items:
            problems[item.id] = chr(ord('a') + item.index - 1)
            print('Problem', problems[item.id], 'is', item.id)

        # get participants
        offset = 0
        size = 100
        participants = dict({})

        while True:
            out = self.client.ListParticipants(
                ListProblemsInput(contest_id=self.contest_id, offset=offset, size=size))
            for item in out.items:
                print(item)
                participants[item.id] = item.display_name

            offset += len(out.items)

            if offset >= out.total:
                break

        # get submissions
        offset = 0
        size = 100

        solutions = []

        while True:
            ee = expression_pb2.ExpressionEnum(value="ERROR")
            setattr(ee, "is", 2)

            listing = self.client.ListSubmissions(ListSubmissionsInput(
                contest_id=self.contest_id,
                size=size,
                offset=offset,
                extra=[Submission.SOURCE]
            ))

            for submission in listing.items:
                if submission.submitted_at.seconds > ends_at:
                    continue
                solutions += [submission]
                username = submission.participant_id
                if submission.participant_id in participants:
                    username = participants[submission.participant_id]

                path = "{}/{}/{}/{}_{}.{}".format(
                    dest,
                    username,
                    problems[submission.problem_id],
                    submission.submitted_at.seconds,
                    math.floor(submission.percentage * 100),
                    langext(submission.lang)
                )

                os.makedirs(os.path.dirname(path), exist_ok=True)

                if store_data:
                    with open(path, "w") as file1:
                        link = submission.source_url
                        data = 0
                        if link.startswith("data:text/plain;base64,"):
                            # Base64 data URI handling
                            base64_data = link.split(",", 1)[1]
                            try:
                                data = base64.b64decode(base64_data).decode('utf-8')
                            except Exception as e:
                                print(f"Error decoding Base64: {e}")
                        else:
                            data = requests.get(submission.source_url).text

                        file1.write(data)

                # print(submission.id, "saved to", path)

            offset += len(listing.items)
            if offset >= listing.total:
                break
            print(offset, '/', listing.total)

        return solutions

    def download_logs(self):
        print('downloading start')
        start = self.contest.starts_at.seconds

        participant_start = {}

        def get_participants(offset, size):
            return self.client.ListParticipants(
                request=ListParticipantsInput(contest_id=self.contest.id, offset=offset, size=size))

        participants = {}
        for participant in self.get_many(get_participants):
            if self.contest.participation_mode == 2:
                participant_start[participant.id] = participant.started_at.seconds
            else:
                participant_start[participant.id] = start
            participants[participant.id] = participant.name

        problems = {}
        problem_list = self.client.ListProblems(ListProblemsInput(contest_id=self.contest.id)).items
        problem_index = 0
        for problem in problem_list:
            problems[problem.id] = chr(ord('A') + problem_index)
            problem_index += 1

        print(self.contest)

        print(start)

        def get_submissions(offset, size):
            return self.client.ListSubmissions(
                request=ListSubmissionsInput(contest_id=self.contest_id, offset=offset, size=size))

        submissions = self.get_many(get_submissions)
        f = open("./logs.csv", "w")
        f.write("Run_Id;User_Name;Prob;Dur_Hour;Dur_Min;Dur_Sec;Stat_Short;\n")
        for sub in reversed(submissions):
            print(sub)
            if sub.submitted_at.seconds > self.contest.ends_at.seconds:
                continue
            if sub.status == 4:
                if hasattr(sub, 'percentage') and sub.percentage < 0.99999:
                    stat = "RJ"
                else:
                    stat = "OK"
            else:
                stat = "CE"

            seconds = sub.submitted_at.seconds - participant_start[sub.participant_id]

            minutes = seconds // 60
            seconds %= 60
            hours = minutes // 60
            minutes %= 60
            problem = problems[sub.problem_id]

            name = participants[sub.participant_id]
            f.write("0;" + name + ";" + problem + ";" + str(hours) + ";" + str(minutes) + ";" + str(
                seconds) + ";" + stat + ";\n")
        print('done')
        f.close()

    def find_full_solutions(self, problem, score=100):
        offset = 0
        size = 100
        list = []
        exp = expression_pb2.ExpressionFloat(value=score)
        setattr(exp, 'is', 1)
        exp2 = expression_pb2.ExpressionID(value=problem)
        setattr(exp2, 'is', 1)
        filter = ListSubmissionsInput.Filter(score=[exp], problem_id=[exp2])
        while True:

            time.sleep(1)

            listing = self.client.ListSubmissions(
                ListSubmissionsInput(contest_id=self.contest.id, size=size,
                                     offset=offset, filters=filter, extra=[Submission.SOURCE]))
            print(listing)
            for sub in listing.items:
                print(sub)
                list.append(sub)

            offset += size
            if offset > listing.total:
                break
        return list

    def blur_contest(self, contest_id):
        self.client.UpdateContest(UpdateContestInput(contest_id=contest_id,
                                                     patch=[UpdateContestInput.SCOREBOARD],
                                                     contest=Contest(
                                                         scoreboard=Contest.Scoreboard(
                                                             visibility=Contest.Scoreboard.INVISIBLE,
                                                             no_spoiler_ui=True
                                                         )
                                                     )))
