package services

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"evening_detective/internal/models"
	"evening_detective/internal/modules/password"
	"evening_detective/internal/services/db"
	"evening_detective/internal/services/link"
	"evening_detective/internal/services/pdf"
	"evening_detective/internal/services/story_service"
	"evening_detective/proto"
	"fmt"
	"time"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

type Services struct {
	dbService         db.IDBService
	storyService      *story_service.StoryService
	linkService       link.ILinkService
	passwordGenerator password.IPasswordGenerator
	pdfGenerator      pdf.IPDFGenerator
}

func NewServices(
	dbService db.IDBService,
	storyService *story_service.StoryService,
	linkService link.ILinkService,
	passwordGenerator password.IPasswordGenerator,
	pdfGenerator pdf.IPDFGenerator,
) *Services {
	return &Services{
		dbService:         dbService,
		storyService:      storyService,
		linkService:       linkService,
		passwordGenerator: passwordGenerator,
		pdfGenerator:      pdfGenerator,
	}
}

func (s *Services) GiveApplications(ctx context.Context, req *proto.GiveApplicationsReq) (*proto.GiveApplicationsRsp, error) {
	applications := mapProtoApplicationsToApplications(req.Applications)
	if err := s.dbService.GiveApplications(ctx, req.TeamId, applications); err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	return &proto.GiveApplicationsRsp{}, nil
}

func (s *Services) GetGame(ctx context.Context, _ *proto.GetGameReq) (*proto.GetGameRsp, error) {
	game, err := s.dbService.GetGame(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	return &proto.GetGameRsp{
		State:   game.State,
		StartAt: game.StartTime,
		EndAt:   game.EndTime,
	}, nil
}

func (s *Services) GameStart(ctx context.Context, _ *proto.GameStartReq) (*proto.GameStartRsp, error) {
	if err := s.dbService.UpdateGameState(ctx, "RUN"); err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	return &proto.GameStartRsp{}, nil
}

func (s *Services) GameStop(ctx context.Context, req *proto.GameStopReq) (*proto.GameStopRsp, error) {
	if err := s.dbService.UpdateGameState(ctx, "STOP"); err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	return &proto.GameStopRsp{}, nil
}

func (s *Services) AddAction(ctx context.Context, req *proto.AddActionReq) (*proto.AddActionRsp, error) {
	team, err := s.getTeam(ctx)
	if err != nil {
		return nil, err
	}
	place := s.storyService.GetPlace(req.Place)
	actions := []*models.Action{
		{
			Place:        place.Code,
			TeamID:       team.ID,
			Applications: mapStoryApplicationsToApplications(place.Applications),
		},
	}
	if err := s.dbService.AddActions(ctx, team.ID, actions); err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	currentApplications, err := s.dbService.GetApplications(ctx, team.ID)
	if err != nil {
		return nil, err
	}

	newApplications := make([]*models.Application, 0, len(actions))
	for _, action := range actions {
		for _, actionApplication := range action.Applications {
			f := false
			for _, currentApplication := range currentApplications {
				if currentApplication.Name == actionApplication.Name {
					f = true
				}
			}
			if !f {
				newApplications = append(newApplications, actionApplication)
			}
		}
	}

	if err := s.dbService.AddApplications(ctx, team.ID, newApplications); err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	addLog(team, "add action", actions)
	return &proto.AddActionRsp{}, nil
}

func (s *Services) GetTeam(ctx context.Context, req *proto.GetTeamReq) (*proto.GetTeamRsp, error) {
	team, err := s.getTeam(ctx)
	if err != nil {
		return nil, err
	}

	actions, err := s.dbService.GetActions(ctx, team.ID)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	actionsCodes := make([]string, 0, len(actions))
	for _, action := range actions {
		actionsCodes = append(actionsCodes, action.Place)
	}

	res := make([]*proto.Action, 0, len(actions))
	for i, place := range s.storyService.GetPlaces(actionsCodes) {
		newAction := mapPlaceToProtoAction(place)
		newAction.Id = actions[i].ID
		newAction.Name = place.Name
		newAction.Text = place.Text
		newAction.Image = place.Image
		newAction.Applications = make([]*proto.Application, 0, len(place.Applications))
		for _, application := range place.Applications {
			newAction.Applications = append(newAction.Applications, mapStoryApplicationToProtoApplication(application))
		}
		newAction.Hidden = place.Hidden
		newAction.Doors = make([]*proto.Door, 0, len(place.Doors))
		for _, door := range place.Doors {
			newAction.Doors = append(newAction.Doors, mapStoryDoorToProtoDoor(door))
		}
		res = append(res, newAction)
	}
	return &proto.GetTeamRsp{
		Name:    team.Name,
		Actions: res,
	}, err
}

func (s *Services) GetTeamsCSV(ctx context.Context, req *proto.GetTeamsCSVReq) (*proto.GetTeamsCSVRsp, error) {
	panic("unimplemented")
}

func (s *Services) GetTeams(ctx context.Context, _ *proto.GetTeamsReq) (*proto.GetTeamsRsp, error) {
	teams, err := s.dbService.GetTeams(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	res := make([]*proto.TeamAdvanced, 0, len(teams))
	for _, team := range teams {
		newTeam := mapTeamsToTeamAdvanced(team)
		actions, err := s.dbService.GetActions(ctx, team.ID)
		if err != nil {
			return nil, err
		}
		newTeam.Url = s.linkService.GetTeamClientLink(team.Name, team.Password)
		newTeam.SpendTime = int64(len(actions))
		currentApplications, err := s.dbService.GetApplicationsByState(ctx, team.ID, "NEW")
		if err != nil {
			return nil, err
		}
		newTeam.Applications = mapApplicationsToProtoApplications(currentApplications)
		res = append(res, newTeam)
	}
	return &proto.GetTeamsRsp{Teams: res}, err
}

func (s *Services) AddTeams(ctx context.Context, req *proto.AddTeamsReq) (*proto.AddTeamsRsp, error) {
	inTeams := make([]*models.Team, 0, len(req.Teams))
	for _, team := range req.Teams {
		t := mapProtoTeamsToTeam(team)
		t.Password = s.passwordGenerator.GeneratePassword(8)
		inTeams = append(inTeams, t)
	}
	teams, err := s.dbService.AddTeams(ctx, inTeams)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	res := make([]*proto.TeamFull, 0, len(teams))
	for _, team := range teams {
		res = append(res, mapTeamsToTeamFull(team))
	}
	return &proto.AddTeamsRsp{Teams: res}, nil
}

func (s *Services) DownloadTeamsQrCodesFile(ctx context.Context, req *proto.DownloadTeamsQrCodesFileReq) (*proto.DownloadTeamsQrCodesFileRsp, error) {
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()
	teams, err := s.dbService.GetTeams(ctx)
	if err != nil {
		return nil, err
	}
	for _, team := range teams {
		team.Link = s.linkService.GetTeamClientLink(team.Name, team.Password)
	}
	b, err := s.pdfGenerator.CreateTeamsPDF(teams)
	if err != nil {
		return nil, err
	}
	return &proto.DownloadTeamsQrCodesFileRsp{Result: b}, nil
}

func (s *Services) UpdateNode(ctx context.Context, req *proto.UpdateNodeReq) (*proto.UpdateNodeRsp, error) {
	applications := make([]*story_service.GraphApplication, 0, len(req.Node.Applications))
	for _, application := range req.Node.Applications {
		applications = append(
			applications,
			&story_service.GraphApplication{
				Name: application.Name,
			},
		)
	}
	node := &story_service.GraphNode{
		Code:         req.Node.Code,
		Name:         req.Node.Name,
		Text:         req.Node.Text,
		Applications: applications,
	}
	if err := s.storyService.UpdatePlace(ctx, req.Code, node); err != nil {
		return nil, err
	}
	return &proto.UpdateNodeRsp{}, nil
}

func (s *Services) GetGraph(ctx context.Context, req *proto.GetGraphReq) (*proto.GetGraphRsp, error) {
	graph := s.storyService.GetGraph(ctx)
	nodes := make([]*proto.GraphNode, 0, len(graph.Nodes))
	for _, node := range graph.Nodes {
		applications := make([]*proto.GraphApplication, 0, len(node.Applications))
		for _, application := range node.Applications {
			applications = append(
				applications,
				&proto.GraphApplication{
					Name: application.Name,
				},
			)
		}
		nodes = append(nodes, &proto.GraphNode{
			Code:         node.Code,
			Name:         node.Name,
			Text:         node.Text,
			Applications: applications,
		})
	}
	edges := make([]*proto.GetGraphRsp_Edge, 0, len(graph.Edges))
	for _, edge := range graph.Edges {
		edges = append(edges, &proto.GetGraphRsp_Edge{
			From:   edge.From,
			To:     edge.To,
			Arrows: "to",
			Type:   edge.Type,
		})
	}
	return &proto.GetGraphRsp{
		Nodes:      nodes,
		Edges:      edges,
		CountNodes: int32(len(nodes)),
		CountEdges: int32(len(edges)),
	}, nil
}

func (s *Services) getTeam(ctx context.Context) (*models.Team, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, status.Errorf(codes.Unauthenticated, "error creds")
	}

	teamIdArr, ok := md["team-id"]
	if !ok {
		return nil, status.Errorf(codes.Unauthenticated, "error creds")
	}
	teamId, err := base64.StdEncoding.DecodeString(teamIdArr[0])
	if err != nil {
		return nil, status.Errorf(codes.Unauthenticated, "error creds")
	}

	passwordArr, ok := md["password"]
	if !ok {
		return nil, status.Errorf(codes.Unauthenticated, "error creds")
	}
	password := passwordArr[0]

	team, err := s.dbService.GetTeam(ctx, teamId, password)
	if err != nil {
		return nil, status.Errorf(codes.Unauthenticated, err.Error())
	}
	return team, nil
}

func addLog(team *models.Team, action string, v any) {
	vJson, err := json.Marshal(v)
	if err == nil {
		fmt.Printf("Team %s: %s %s\n", team.Name, action, string(vJson))
	}
}
