import time
import random
import numpy as np
from mcp.types import ToolAnnotations
from mcp.server.fastmcp import FastMCP
import json
import requests
import sys

baseAddress="https://pedrogoserver.pythonanywhere.com/"

def getKomiFromHTTPS()->float:
    try:
        req=requests.get(baseAddress+"komi",timeout=60)
        ret=req.content
        ret=float(ret)
        return ret
    except:
        print("I could not get the komi from the server.",file=sys.stderr)
        pass
    return 1.5

KOMI = getKomiFromHTTPS()
SIDE = 9
ESPACO = '.'
BLACK = 'O'
WHITE = 'X'
PASS = "pass"
DBNAME="go.json"

server=FastMCP("go game robot")
print(server,file=sys.stderr)

class Coordenada:
    def __init__(self, coluna, linha=None):
        if linha is not None:
            self.x = coluna
            self.y = linha
        else:
            self.x = ord(coluna[0]) - ord('a')
            self.y = int(coluna[1:]) - 1
    def isValid(self):
        return 0 <= self.x < SIDE and 0 <= self.y < SIDE
    def getNeighbors(self):
        neighbors = []
        if self.y > 0:
            neighbors.append(Coordenada(self.x, self.y - 1))
        if self.x < (SIDE - 1):
            neighbors.append(Coordenada(self.x + 1, self.y))
        if self.y < (SIDE - 1):
            neighbors.append(Coordenada(self.x, self.y + 1))
        if self.x > 0:
            neighbors.append(Coordenada(self.x - 1, self.y))
        return neighbors
    def __str__(self):
        return chr(self.x + ord('a')) + str(self.y + 1)
    def __eq__(self, other):
        if other==None:
            return False
        return self.x == other.x and self.y == other.y

def mirrorCoordinateRight(coor):
    return Coordenada (SIDE-1-coor.x, coor.y)
def mirrorCoordinateUp(coor):
    return Coordenada (coor.x, SIDE-1-coor.y)
def mirrorCoordinateRightUp(coor):
    return Coordenada (SIDE-1-coor.x, SIDE-1-coor.y)
def transposeCoordinate(coor):
    return Coordenada (coor.y, coor.x)

class Tabuleiro:
    def __init__(self, linhas=None, history=None, images=None):
        if type(linhas)==np.ndarray:
            self.linhas=linhas
            self.history=history
            self.images=images
            return
        self.linhas = np.full((SIDE, SIDE), ESPACO, dtype='<U1')
        self.history = []
        self.images = []
    def vez(self):
        return WHITE if len(self.history) % 2 else BLACK
    def isKo(self):
        if not self.history:
            return False
        if self.history[-1]==PASS:
            return False
        start=1 if self.vez()==BLACK else 0
        for i in range(start,len(self.images)-1,2):
            img=self.images[i]
            if np.array_equal(img,self.linhas):
                return True
        return False
    def back(self,how_many=1):
        if not self.history:
            return
        self.history=self.history[:-how_many]
        self.images=self.images[:-how_many]
        if self.images:
            self.linhas=np.copy(self.images[-1])
        else:
            self.linhas = np.full((SIDE, SIDE), ESPACO, dtype='<U1')
    def isGameOver(self):
        return len(self.history) >= 2 and self.history[-1] == PASS and self.history[-2] == PASS
    def __str__(self):
        return '\n'.join(''.join(row) for row in self.linhas)
    def getGroup(self,coor,ret=None):
        color=self.linhas[coor.y][coor.x]
        if ret==None:
            ret=[coor]
        neighbors=coor.getNeighbors()
        for neigh in neighbors:
            if self.linhas[neigh.y][neigh.x]==color:
                if neigh not in ret:
                    ret.append(neigh)
                    self.getGroup(neigh,ret)
        return ret
    def getLiberties(self,group):
        ret=[]
        for stone in group:
            neighbors=stone.getNeighbors()
            for neigh in neighbors:
                if self.linhas[neigh.y][neigh.x]==ESPACO:
                    if neigh not in ret:
                        ret.append(neigh)
        return ret
    def play(self, coor):
        if type(coor) == str:
            if coor == PASS:
                self.history.append(PASS)
                self.images.append(np.copy(self.linhas))
                return True
            return self.play(Coordenada(coor))
        if not coor.isValid() or self.linhas[coor.y, coor.x] != ESPACO:
            return False
        removed=[]
        vez=self.vez()
        self.linhas[coor.y][coor.x]=vez
        neighbors=coor.getNeighbors()
        for neigh in neighbors:
            if self.linhas[neigh.y][neigh.x]==ESPACO:
                continue
            if self.linhas[neigh.y][neigh.x]==vez:
                continue
            group=self.getGroup(neigh)
            liberties=self.getLiberties(group)
            if len(liberties)==0:
                if group[0] not in removed:
                    removed.extend(group)
        if len(removed)==0:
            group=self.getGroup(coor)
            if len(self.getLiberties(group))==0:
                self.linhas[coor.y][coor.x]=ESPACO
                return False
        s=str(coor)
        if len(removed):
            for r in removed:
                self.linhas[r.y][r.x]=ESPACO
        self.history.append(s)
        self.images.append(np.copy(self.linhas))
        if self.isKo():
            self.back()
            return False
        return True
    def count(self):
        black=0
        white=KOMI
        DAME="dame"
        empties=dict()
        empties[BLACK]=[]
        empties[WHITE]=[]
        empties[DAME]=[]
        def insert_group(color,group):
            l=empties[color]
            for space in group:
                l.append((space.x,space.y))
        for y in range(SIDE):
            for x in range(SIDE):
                color=self.linhas[y][x]
                if color==BLACK:
                    black+=1
                    continue
                elif color==WHITE:
                    white+=1
                    continue
                t=(x,y)
                if (t in empties[BLACK]) or (t in empties[WHITE]) or (t in empties[DAME]):
                    continue
                coor=Coordenada(x,y)
                group=self.getGroup(coor)
                hbn=False
                hwn=False
                for space in group:
                    neighbors=space.getNeighbors()
                    for neigh in neighbors:
                        c=self.linhas[neigh.y][neigh.x]
                        if c==ESPACO:
                            continue
                        if c==BLACK:
                            hbn=True
                        else:
                            hwn=True
                if hbn==hwn:
                    insert_group(DAME,group)
                elif hbn:
                    insert_group(BLACK,group)
                else:
                    insert_group(WHITE,group)
        black+=len(empties[BLACK])
        white+=len(empties[WHITE])
        return black,white

def mirrorsGame(t:Tabuleiro):
    ret=[]
    for _ in range(7):
        ret.append(Tabuleiro())
    for hist in t.history:
        if hist==PASS:
            for g in ret:
                g.play(PASS)
            continue
        coor=Coordenada(hist)
        ret[0].play(mirrorCoordinateRight(coor))
        ret[1].play(mirrorCoordinateUp(coor))
        ret[2].play(mirrorCoordinateRightUp(coor))
        trans=transposeCoordinate(coor)
        ret[3].play(trans)
        ret[4].play(mirrorCoordinateRight(trans))
        ret[5].play(mirrorCoordinateUp(trans))
        ret[6].play(mirrorCoordinateRightUp(trans))
    return ret

def endsWith (s,needle):
    return s[-len(needle):]==needle

def baixar_banco():
    url = baseAddress+"json"
    nome_arquivo = DBNAME
    try:
        resposta = requests.get(url, stream=True, timeout=60)
        resposta.raise_for_status()
    except:
        return
    with open(nome_arquivo, "wb") as f:
        for chunk in resposta.iter_content(chunk_size=8192):
            if chunk:
                f.write(chunk)
    resposta.close()

previousGames=[]

def loadPreviousGames():
        global previousGames
        jss=None
        try:
            with open(DBNAME,"rt") as file:
                jss=file.read()
        except:
            print(f"I could not open or read {DBNAME}", file=sys.stderr)
            exit()
        js=None
        try:
            js=json.loads(jss)
        except Exception as e:
            print (f"{DBNAME} is not a JSON.", file=sys.stderr)
            exit()
        jss=None
        if type(js)!=list:
            print (f"{DBNAME} is not an array.", file=sys.stderr)
            exit()
        histories=[]
        for i,row in enumerate(js):
            if type(row)!=list:
                print (f"row {i} is not an array. It is:\n{row}", file=sys.stderr)
                exit()
            if len(row)<3:
                print (f"row {i} does not contain 3 elements.", file=sys.stderr)
                exit()
            size = row[1]
            if (type(size)!=int):
                print (f"row {i} second element is not an integer. It is:\n{size}", file=sys.stderr)
                exit()
            if size!=9:
                continue
            history = row[2]
            if type(history)!=str:
                print (f"row {i} third element is not a string. It is:\n{history}", file=sys.stderr)
                exit()
            histories.append(history)
        for history in histories:
            invalid=False
            if not endsWith(history,"pass;pass"):
                continue
            t=Tabuleiro()
            history = history.split(";")
            for hist in history:
                if t.play(hist)==False:
                    invalid=True
                    break
            if invalid:
                continue
            previousGames.append(t)
            previousGames.extend(mirrorsGame(t))
        if len(previousGames)==0:
            raise Exception("Bug in method loadPreviousGames()")

class Game:
    def __init__(self, human_color):
        self.id=hex(random.randint(1,2**63-1))
        self.tabuleiro=Tabuleiro()
        self.human_color=human_color
        self.publish:bool = True
        self.published=None
    def publishHistory(self):
        if self.published is not None:
            e=RuntimeError("This game was published before.")
            raise e
        history=self.tabuleiro.history
        url=baseAddress+"links?side="+str(SIDE)
        response=requests.get(url=url,timeout=60)
        rsc=response.status_code
        if rsc!=200:
            e=RuntimeError("links status code="+str(rsc))
            raise e
        links_html=response.text
        tid=""
        bid=""
        wid=""
        li=links_html.index("tid=")+4
        while links_html[li].isdigit():
            tid+=links_html[li]
            li+=1
        li=links_html.index("bid=")+4
        while links_html[li].isdigit():
            bid+=links_html[li]
            li+=1
        li=links_html.index("wid=")+4
        while links_html[li].isdigit():
            wid+=links_html[li]
            li+=1
        vez=BLACK
        for h in history:
            url=baseAddress+"table?tid="+tid
            if vez==BLACK:
                url+="&bid="+bid
            else:
                url+="&wid="+wid
            url+="&mov="+h
            response=requests.get(url,timeout=60)
            rsc=response.status_code
            if rsc!=200:
                e=RuntimeError("mov="+h+
                "\tstatus code="+str(rsc))
                raise e
            if vez==BLACK:
                vez=WHITE
            else:
                vez=BLACK
        html=response.text
        if "<p>Game Over</p>" in html:
            self.published=tid
            previousGames.append(self.tabuleiro)
            previousGames.extend(mirrorsGame(self.tabuleiro))
        elif "<p>This match was deleted.</p>" in html:
            self.published="deleted"
        else:
            self.published="error"
        return self.published

def rememberPreviousGames(partida:Game)->str:
    tabuleiro=partida.tabuleiro
    vez=tabuleiro.vez()
    statistics=dict()
    for game in previousGames:
        if len(game.history)<=len(tabuleiro.history):
            continue
        if tabuleiro.history and not np.array_equal (tabuleiro.linhas, game.images[len(tabuleiro.history)-1]):
            continue
        b,w=game.count()
        if vez==BLACK:
            n=b-w
        else:
            n=w-b
        jogada=game.history[len(tabuleiro.history)]
        if jogada not in statistics:
            statistics[jogada]=0
        if n>0:
            statistics[jogada]+=1
        else:
            statistics[jogada]-=1
    if len(statistics)==0:
        return None
    m=max(statistics.values())
    if m<0:
        return PASS
    for jogada,n in statistics.items():
        if n==m:
            return jogada

def getEmptyCoordinate(t):
    empty_cells = np.argwhere(t.linhas == ESPACO)
    if empty_cells.size == 0:
        return PASS
    i=random.randint(0, len(empty_cells) - 1)
    y, x = empty_cells[i]
    return Coordenada(x, y)

def think(g:Game, depth:int, ttt:int)->str:
    t=g.tabuleiro
    timeLimit = int(time.time()) + ttt
    cp = t.vez()
    if len(t.history) and t.history[-1]==PASS:
        b, w = t.count()
        originalNote = (b - w) if (cp == BLACK) else (w - b)
        if originalNote > 0:
            return PASS
    jogada=rememberPreviousGames(g)
    if jogada and (jogada!=PASS):
        return jogada
    probabilities = dict()
    lenthistory=len(t.history)
    while time.time() < timeLimit:
        for _ in range(depth):
            if t.isGameOver():
                break
            coor = getEmptyCoordinate(t)
            if t.play(coor)==False:
                t.play(PASS)
        b, w = t.count()
        n = (b - w) if (cp == BLACK) else (w - b)
        score = 1 if n > 0 else -1
        how_many=len(t.history)-lenthistory
        jogada:str=PASS
        if how_many:
            jogada = t.history[lenthistory]
            t.back(how_many=how_many)
        probabilities[jogada]=probabilities.get(jogada,0)+score
    m=max(probabilities,key=probabilities.get,default=PASS)
    if m==PASS:
        return m
    if probabilities[m]<0:
        return PASS
    return m

currentGames=dict()

@server.tool()
def get_komi() -> str:
    '''Returns the komi. The komi is the white's advantage for being the second to play.'''
    return str(KOMI)+" points."

@server.tool()
def switch_publish(game_id:str) -> str:
    '''Turns publishing on or off and returns a str describing the result. The default publishing status is on. Does not affect a match if it already ended.'''
    game=None
    try:
         game=currentGames[game_id]
    except:
        return "error: no such game_id"
    if game.publish:
        game.publish=False
        return "I will not publish."
    else:
        game.publish=True
        return "I will publish."

@server.tool()
def get_table_id(game_id:str) -> str|None:
    '''Returns the table id of the game_id or the word 'deleted' or the word 'error' or None. Only a published match has a table id.'''
    game=None
    try:
        game=currentGames[game_id]
    except:
        return "error: no such game_id"
    return game.published

@server.tool(
    name="new_go_match",
    description="Prepares the 9x9 go board and if the human will play white then it plays the first black stone.",
    annotations=ToolAnnotations(inputSchema={
        "type": "object",
        "properties": {
            "human_color": {
                "type": "string",
                "enum": ["black", "white"],
                "description": "Color of the stones of the human player."
            }
        },
        "required": ["human_color"]
    }
))
def new_go_match (human_color: str) -> str:
    print(f"I am inside new_go_match({human_color}).",file=sys.stderr)
    colors={"black":BLACK,"white":WHITE}
    if human_color not in colors:
        return "error: invalid color."
    game=Game(colors[human_color])
    while game.id in currentGames:
        game.id=hex(random.randint(1,2**63-1))
    currentGames[game.id]=game
    if human_color=="black":
        return f"The id of this game is {game.id} .\nYou play first."
    else:
        jogada=think(game,SIDE*SIDE,60)
        game.tabuleiro.play(jogada)
        return f"The id of this game is {game.id} .\nI played {jogada} ."

@server.tool()
def play (game_id:str, move:str)->str:
    '''
    Plays the move for the human and, if the game is not over, a bot's move. The move may be a coordinate or the word pass . If the move is a coordinate then a stone is placed on the board. If the move is pass then the human passes.
    '''
    try:
        game=currentGames[game_id]
    except:
        return "error: no such game_id"
    if not move:
        return "error: You did not provide the argument move ."
    t=game.tabuleiro
    if t.isGameOver():
        return "The game is over."
    move=move.lower().strip()
    try:
        if not t.play(move):
            return "error: invalid move"
    except:
        return "error: I did not understand your move."
    minor=""
    if t.isGameOver():
        if game.publish:
            try:
                game.publishHistory()
            except:
                minor=" I could not publish this match."
        b,w=t.count()
        return f"The game is over. Black made {b} points. White made {w} points.{minor}"
    jogada=think(game,SIDE*SIDE,60)
    t.play(jogada)
    if t.isGameOver():
        if game.publish:
            try:
                game.publishHistory()
            except:
                minor=" I could not publish this match."
        b,w=t.count()
        return f"I {jogada} .\nThe game is over. Black made {b} points. White made {w} points.{minor}"
    if jogada==PASS:
        ret="I pass ."
    else:
        ret="I played at "+jogada+" ."
    return ret

@server.tool()
def count_the_points(game_id:str)->str:
    '''Counts the points of each color.'''
    try:
        game=currentGames[game_id]
    except:
        return "error: no such game_id"
    b,w=game.tabuleiro.count()
    return f"Black has {b} points. White has {w} points."

@server.tool()
def is_game_over(game_id:str)->bool:
    '''Returns True if a match was started and it is over. Otherwise returns False.'''
    try:
        game=currentGames[game_id]
    except:
        return False
    return game.tabuleiro.isGameOver()

def gos_get_owner (game, gos):
    if len(gos)==81:
        return "neutral"
    neighbors_of_color=[]
    for spc in gos:
        neighbors=spc.getNeighbors()
        for neigh in neighbors:
            if game.tabuleiro.linhas[neigh.y][neigh.x]!=ESPACO:
                if neigh in neighbors_of_color:
                    continue
                else:
                    neighbors_of_color.append(neigh)
    ret=None
    for coor in neighbors_of_color:
        color=game.tabuleiro.linhas[coor.y][coor.x]
        if ret==None:
            ret=color
        else:
            if color==ret:
                continue
            else:
                return "neutral"
    return "black" if ret==BLACK else "white"

@server.tool()
def get_group_informations (game_id:str, coordinate:str)->dict:
    '''Returns some informations about a group of stones or of spaces. The dict that will be returned contains str keys only and str values only.'''
    try:
        game=currentGames[game_id]
    except:
        return {"error": "no such game_id"}
    try:
        coor=Coordenada(coordinate.lower())
    except:
        return {"error":str(coordinate)+" does not represent a Coordenada."}
    if not coor.isValid():
        return {"error":coordinate+" is invalid."}
    t=game.tabuleiro
    color=t.linhas[coor.y][coor.x]
    if color==ESPACO:
        engColor="space"
    else:
        engColor="black" if color==BLACK else "white"
    ret=dict()
    ret["color"]=engColor
    ret["connected_to"]=t.getGroup(coor)
    if color!=ESPACO:
        liberties=t.getLiberties(ret["connected_to"])
        if liberties:
            ret["liberties"]=""
            for liber in liberties:
                ret["liberties"]+=str(liber)+","
            ret["liberties"]=ret["liberties"][:-1]
    else:
        ret["territory_of"] = gos_get_owner (game, ret["connected_to"])
    connected_to=""
    for co in ret["connected_to"]:
        connected_to+=str(co)+","
    if connected_to:
        connected_to=connected_to[:-1]
    ret["connected_to"]=connected_to
    return ret

@server.tool()
def get_match_history(game_id:str)->str:
    '''Returns the history of the game_id provided.'''
    try:
        game=currentGames[game_id]
    except:
        return "error: no such game_id"
    t=game.tabuleiro
    ret=",".join(t.history)
    return ret

@server.tool()
def take_back(game_id:str)->str:
    "Rolls back the match to remove the last human`s move. If the human didn`t play yet then returns an error."
    try:
        game=currentGames[game_id]
    except:
        return "error: no such game_id"
    if ((game.human_color==BLACK) and not game.tabuleiro.history) or ((game.human_color==WHITE) and (len(game.tabuleiro.history)==1)):
        return "error: You did not play yet."
    if game.tabuleiro.isGameOver():
        return "error: The match is over, so it can not be rolled back."
    game.tabuleiro.back()
    if game.tabuleiro.history:
        game.tabuleiro.back()
    return "Ok. The match was rolled back."

@server.tool()
def get_board_ASCII_representation (game_id:str)->str:
    '''
    Returns an ASCII representation of the game_id's board with the stones on it.
    '''
    try:
        game=currentGames[game_id]
    except:
        return "error: no such game_id"
    linhas=game.tabuleiro.linhas
    ret=""
    for y in range(SIDE):
        for x in range(SIDE):
            c=linhas[y][x]
            ret+=c
        ret+="\n"
    return ret
    
if __name__=="__main__":
    baixar_banco()
    loadPreviousGames()
    server.run(transport="stdio")
    print("server: ending execution.",file=sys.stderr)
