package services

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"evening_detective/internal/models"
	"evening_detective/internal/modules/password"
	"evening_detective/internal/services/story_service"
	"evening_detective/proto"
	"fmt"
	"net"
	"net/url"

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

type Services struct {
	repository   *Repository
	storyService *story_service.StoryService
}

func NewServices(
	repository *Repository,
	storyService *story_service.StoryService,
) *Services {
	return &Services{
		repository:   repository,
		storyService: storyService,
	}
}

func (s *Services) GiveApplications(ctx context.Context, req *proto.GiveApplicationsReq) (*proto.GiveApplicationsRsp, error) {
	applications := mapProtoApplicationsToApplications(req.Applications)
	if err := s.repository.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) {
	state, err := s.repository.GetGame(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	return &proto.GetGameRsp{State: state}, nil
}

func (s *Services) GameStart(ctx context.Context, _ *proto.GameStartReq) (*proto.GameStartRsp, error) {
	if err := s.repository.GameUpdateState(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.repository.GameUpdateState(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.repository.AddActions(ctx, team.ID, actions); err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	if err := s.repository.AddApplications(ctx, actions); err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	log(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.repository.GetActions(ctx, team.ID)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}
	res := make([]*proto.Action, 0, len(actions))
	for _, action := range actions {
		newAction := mapActionToProtoAction(action)
		place := s.storyService.GetPlace(action.Place)
		newAction.Text = place.Text
		newAction.Name = place.Name
		newAction.Applications = make([]*proto.Application, 0, len(place.Applications))
		for _, application := range place.Applications {
			newAction.Applications = append(newAction.Applications, mapStoryApplicationToProtoApplication(application))
		}
		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.repository.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.repository.GetActions(ctx, team.ID)
		if err != nil {
			return nil, err
		}
		newTeam.Url, err = getTeamUrl(team)
		if err != nil {
			return nil, err
		}
		newTeam.SpendTime = int64(len(actions))
		applications, err := s.repository.GetApplications(ctx, team.ID, "NEW")
		if err != nil {
			return nil, err
		}
		newTeam.Applications = mapApplicationsToProtoApplications(applications)
		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 = password.GenPass(8)
		inTeams = append(inTeams, t)
	}
	teams, err := s.repository.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}, err
}

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.repository.GetTeam(ctx, teamId, password)
	if err != nil {
		return nil, status.Errorf(codes.Unauthenticated, err.Error())
	}
	return team, nil
}

func log(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))
	}
}

func getTeamUrl(team *models.Team) (string, error) {
	ips, err := getLocalIPs()
	if err != nil {
		return "", err
	}
	ip := ips[0]
	u := fmt.Sprintf("http://%s:8100?name=%s&password=%s", ip, url.PathEscape(team.Name), team.Password)
	return u, nil
}

func getLocalIPs() ([]string, error) {
	var ips []string
	addrs, err := net.InterfaceAddrs()
	if err != nil {
		return nil, err
	}

	for _, addr := range addrs {
		ipNet, ok := addr.(*net.IPNet)
		if !ok {
			continue
		}

		ip := ipNet.IP
		if ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() {
			continue
		}

		if ipv4 := ip.To4(); ipv4 != nil {
			ips = append(ips, ipv4.String())
		}
	}

	return ips, nil
}
