package grpc

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"strconv"
	"sync"
	"time"

	grpcpkg "google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"stream.api/internal/video/runtime/domain"
	"stream.api/internal/video/runtime/proto"
	"stream.api/internal/video/runtime/services"
)

type Server struct {
	proto.UnimplementedWoodpeckerServer
	proto.UnimplementedWoodpeckerAuthServer
	jobService   *services.JobService
	agentManager *AgentManager
	agentSecret  string
	sessions     sync.Map
	agentJobs    sync.Map
	onAgentEvent func(string, *services.AgentWithStats)
}

func NewServer(jobService *services.JobService, agentSecret string) *Server {
	return &Server{jobService: jobService, agentManager: NewAgentManager(), agentSecret: agentSecret}
}

func (s *Server) SetAgentEventHandler(handler func(string, *services.AgentWithStats)) {
	s.onAgentEvent = handler
}

func (s *Server) Register(grpcServer grpcpkg.ServiceRegistrar) {
	proto.RegisterWoodpeckerServer(grpcServer, s)
	proto.RegisterWoodpeckerAuthServer(grpcServer, s)
}

func (s *Server) SendCommand(agentID string, cmd string) bool {
	return s.agentManager.SendCommand(agentID, cmd)
}

func (s *Server) ListAgents() []*domain.Agent { return s.agentManager.ListAll() }

func (s *Server) ListAgentsWithStats() []*services.AgentWithStats {
	agents := s.agentManager.ListAll()
	result := make([]*services.AgentWithStats, 0, len(agents))
	for _, agent := range agents {
		result = append(result, &services.AgentWithStats{Agent: agent, ActiveJobCount: int64(len(s.getAgentJobs(agent.ID)))})
	}
	return result
}

func (s *Server) getAgentWithStats(agentID string) *services.AgentWithStats {
	for _, agent := range s.ListAgentsWithStats() {
		if agent != nil && agent.Agent != nil && agent.ID == agentID {
			return agent
		}
	}
	return nil
}

func (s *Server) Version(context.Context, *proto.Empty) (*proto.VersionResponse, error) {
	return &proto.VersionResponse{GrpcVersion: 15, ServerVersion: "stream.api"}, nil
}

func generateToken() string {
	b := make([]byte, 16)
	_, _ = rand.Read(b)
	return hex.EncodeToString(b)
}

func generateAgentID() string {
	return strconv.FormatInt(time.Now().UnixNano(), 10)
}

func (s *Server) getAgentIDFromContext(ctx context.Context) (string, string, bool) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return "", "", false
	}
	tokens := md.Get("token")
	if len(tokens) == 0 {
		return "", "", false
	}
	token := tokens[0]
	if id, ok := s.sessions.Load(token); ok {
		return id.(string), token, true
	}
	return "", "", false
}

func (s *Server) Next(context.Context, *proto.NextRequest) (*proto.NextResponse, error) {
	return nil, status.Error(codes.Unimplemented, "use StreamJobs")
}

func (s *Server) StreamJobs(_ *proto.StreamOptions, stream grpcpkg.ServerStreamingServer[proto.Workflow]) error {
	ctx := stream.Context()
	agentID, _, ok := s.getAgentIDFromContext(ctx)
	if !ok {
		return status.Error(codes.Unauthenticated, "invalid or missing token")
	}
	s.agentManager.UpdateHeartbeat(agentID)
	cancelCh, _ := s.jobService.SubscribeCancel(ctx, agentID)
	commandCh, _ := s.agentManager.GetCommandChannel(agentID)
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()
	for {
		select {
		case cmd := <-commandCh:
			payload, _ := json.Marshal(map[string]any{"image": "alpine", "commands": []string{"echo 'System Command'"}, "environment": map[string]string{}, "action": cmd})
			if err := stream.Send(&proto.Workflow{Id: fmt.Sprintf("cmd-%s-%d", agentID, time.Now().UnixNano()), Timeout: 300, Payload: payload}); err != nil {
				return err
			}
		case jobID := <-cancelCh:
			if s.isJobAssigned(agentID, jobID) {
				if err := stream.Send(&proto.Workflow{Id: jobID, Cancel: true}); err != nil {
					return err
				}
			}
		case <-ctx.Done():
			return nil
		case <-ticker.C:
			s.agentManager.UpdateHeartbeat(agentID)
			jobCtx, cancel := context.WithTimeout(ctx, time.Second)
			job, err := s.jobService.GetNextJob(jobCtx)
			cancel()
			if err != nil || job == nil {
				continue
			}
			s.trackJobAssignment(agentID, job.ID)
			if err := s.jobService.AssignJob(ctx, job.ID, agentID); err != nil {
				s.untrackJobAssignment(agentID, job.ID)
				continue
			}
			var config map[string]any
			if err := json.Unmarshal([]byte(job.Config), &config); err != nil {
				_ = s.jobService.UpdateJobStatus(ctx, job.ID, domain.JobStatusFailure)
				s.untrackJobAssignment(agentID, job.ID)
				continue
			}
			image, _ := config["image"].(string)
			if image == "" {
				image = "alpine"
			}
			commands := []string{"echo 'No commands specified'"}
			if raw, ok := config["commands"].([]any); ok && len(raw) > 0 {
				commands = commands[:0]
				for _, item := range raw {
					if text, ok := item.(string); ok {
						commands = append(commands, text)
					}
				}
				if len(commands) == 0 {
					commands = []string{"echo 'No commands specified'"}
				}
			}
			payload, _ := json.Marshal(map[string]any{"image": image, "commands": commands, "environment": map[string]string{}})
			if err := stream.Send(&proto.Workflow{Id: job.ID, Timeout: job.TimeLimit, Payload: payload}); err != nil {
				_ = s.jobService.UpdateJobStatus(ctx, job.ID, domain.JobStatusPending)
				s.untrackJobAssignment(agentID, job.ID)
				return err
			}
		}
	}
}

func (s *Server) SubmitStatus(stream grpcpkg.ClientStreamingServer[proto.StatusUpdate, proto.Empty]) error {
	ctx := stream.Context()
	agentID, _, ok := s.getAgentIDFromContext(ctx)
	if !ok {
		return status.Error(codes.Unauthenticated, "invalid or missing token")
	}
	for {
		update, err := stream.Recv()
		if err != nil {
			return stream.SendAndClose(&proto.Empty{})
		}
		switch update.Type {
		case 0, 1:
			_ = s.jobService.ProcessLog(ctx, update.StepUuid, update.Data)
		case 4:
			var progress float64
			fmt.Sscanf(string(update.Data), "%f", &progress)
			_ = s.jobService.UpdateJobProgress(ctx, update.StepUuid, progress)
		case 5:
			var stats struct {
				CPU float64 `json:"cpu"`
				RAM float64 `json:"ram"`
			}
			if json.Unmarshal(update.Data, &stats) == nil {
				s.agentManager.UpdateResources(agentID, stats.CPU, stats.RAM)
				if s.onAgentEvent != nil {
					s.onAgentEvent("agent_update", s.getAgentWithStats(agentID))
				}
			}
			_ = s.jobService.PublishSystemResources(ctx, agentID, update.Data)
		}
	}
}

func (s *Server) Init(ctx context.Context, req *proto.InitRequest) (*proto.Empty, error) {
	if err := s.jobService.UpdateJobStatus(ctx, req.Id, domain.JobStatusRunning); err != nil {
		return nil, status.Error(codes.Internal, "failed to update job status")
	}
	return &proto.Empty{}, nil
}

func (s *Server) Wait(context.Context, *proto.WaitRequest) (*proto.WaitResponse, error) {
	return &proto.WaitResponse{Canceled: false}, nil
}

func (s *Server) Done(ctx context.Context, req *proto.DoneRequest) (*proto.Empty, error) {
	agentID, _, ok := s.getAgentIDFromContext(ctx)
	if !ok {
		return nil, status.Error(codes.Unauthenticated, "invalid session")
	}
	jobStatus := domain.JobStatusSuccess
	if req.State != nil && req.State.Error != "" {
		jobStatus = domain.JobStatusFailure
	}
	if err := s.jobService.UpdateJobStatus(ctx, req.Id, jobStatus); err != nil {
		return nil, status.Error(codes.Internal, "failed to update job status")
	}
	s.untrackJobAssignment(agentID, req.Id)
	return &proto.Empty{}, nil
}

func (s *Server) Update(context.Context, *proto.UpdateRequest) (*proto.Empty, error) {
	return &proto.Empty{}, nil
}

func (s *Server) Log(ctx context.Context, req *proto.LogRequest) (*proto.Empty, error) {
	if _, _, ok := s.getAgentIDFromContext(ctx); !ok {
		return nil, status.Error(codes.Unauthenticated, "invalid session")
	}
	for _, entry := range req.LogEntries {
		if entry.StepUuid != "" {
			_ = s.jobService.ProcessLog(ctx, entry.StepUuid, entry.Data)
		}
	}
	return &proto.Empty{}, nil
}

func (s *Server) Extend(context.Context, *proto.ExtendRequest) (*proto.Empty, error) {
	return &proto.Empty{}, nil
}

func (s *Server) RegisterAgent(ctx context.Context, req *proto.RegisterAgentRequest) (*proto.RegisterAgentResponse, error) {
	if req.Info == nil {
		return nil, status.Error(codes.InvalidArgument, "connection info is required")
	}
	id, _, ok := s.getAgentIDFromContext(ctx)
	if !ok {
		return nil, status.Error(codes.Unauthenticated, "invalid session")
	}
	hostname := ""
	if req.Info.CustomLabels != nil {
		hostname = req.Info.CustomLabels["hostname"]
	}
	name := hostname
	if name == "" {
		name = fmt.Sprintf("agent-%s", id)
	}
	s.agentManager.Register(id, name, req.Info.Platform, req.Info.Backend, req.Info.Version, req.Info.Capacity)
	if s.onAgentEvent != nil {
		s.onAgentEvent("agent_update", s.getAgentWithStats(id))
	}
	return &proto.RegisterAgentResponse{AgentId: id}, nil
}

func (s *Server) UnregisterAgent(ctx context.Context, _ *proto.Empty) (*proto.Empty, error) {
	agentID, token, ok := s.getAgentIDFromContext(ctx)
	if !ok {
		return nil, status.Error(codes.Unauthenticated, "invalid session")
	}
	for _, jobID := range s.getAgentJobs(agentID) {
		_ = s.jobService.UpdateJobStatus(ctx, jobID, domain.JobStatusFailure)
		s.untrackJobAssignment(agentID, jobID)
	}
	s.sessions.Delete(token)
	s.agentJobs.Delete(agentID)
	agent := s.getAgentWithStats(agentID)
	s.agentManager.Unregister(agentID)
	if s.onAgentEvent != nil {
		s.onAgentEvent("agent_update", agent)
	}
	return &proto.Empty{}, nil
}

func (s *Server) ReportHealth(ctx context.Context, _ *proto.ReportHealthRequest) (*proto.Empty, error) {
	agentID, _, ok := s.getAgentIDFromContext(ctx)
	if !ok {
		return nil, status.Error(codes.Unauthenticated, "invalid session")
	}
	s.agentManager.UpdateHeartbeat(agentID)
	return &proto.Empty{}, nil
}

func (s *Server) Auth(ctx context.Context, req *proto.AuthRequest) (*proto.AuthResponse, error) {
	if s.agentSecret != "" && req.AgentToken != s.agentSecret {
		return nil, status.Error(codes.Unauthenticated, "invalid agent secret")
	}
	agentID := req.AgentId
	if len(agentID) > 6 && agentID[:6] == "agent-" {
		agentID = agentID[6:]
	}
	if agentID == "" {
		agentID = generateAgentID()
	}
	accessToken := generateToken()
	s.sessions.Store(accessToken, agentID)
	return &proto.AuthResponse{Status: "ok", AgentId: agentID, AccessToken: accessToken}, nil
}

func (s *Server) trackJobAssignment(agentID, jobID string) {
	jobSetInterface, _ := s.agentJobs.LoadOrStore(agentID, &sync.Map{})
	if jobSet, ok := jobSetInterface.(*sync.Map); ok {
		jobSet.Store(jobID, true)
	}
}

func (s *Server) untrackJobAssignment(agentID, jobID string) {
	if jobSetInterface, ok := s.agentJobs.Load(agentID); ok {
		if jobSet, ok := jobSetInterface.(*sync.Map); ok {
			jobSet.Delete(jobID)
		}
	}
}

func (s *Server) isJobAssigned(agentID, jobID string) bool {
	if jobSetInterface, ok := s.agentJobs.Load(agentID); ok {
		if jobSet, ok := jobSetInterface.(*sync.Map); ok {
			_, found := jobSet.Load(jobID)
			return found
		}
	}
	return false
}

func (s *Server) getAgentJobs(agentID string) []string {
	jobs := []string{}
	if jobSetInterface, ok := s.agentJobs.Load(agentID); ok {
		if jobSet, ok := jobSetInterface.(*sync.Map); ok {
			jobSet.Range(func(key, _ any) bool {
				if jobID, ok := key.(string); ok {
					jobs = append(jobs, jobID)
				}
				return true
			})
		}
	}
	return jobs
}
