package services

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"clawreef/internal/models"
	"clawreef/internal/repository"
)

const (
	InstanceCommandTypeStartOpenClaw       = "start_openclaw"
	InstanceCommandTypeStopOpenClaw        = "stop_openclaw"
	InstanceCommandTypeRestartOpenClaw     = "restart_openclaw"
	InstanceCommandTypeCollectSystemInfo   = "collect_system_info"
	InstanceCommandTypeApplyConfigRevision = "apply_config_revision"
	InstanceCommandTypeReloadConfig        = "reload_config"
	InstanceCommandTypeHealthCheck         = "health_check"
	InstanceCommandTypeInstallSkill        = "install_skill"
	InstanceCommandTypeUpdateSkill         = "update_skill"
	InstanceCommandTypeUninstallSkill      = "uninstall_skill"
	InstanceCommandTypeRemoveSkill         = "remove_skill"
	InstanceCommandTypeDisableSkill        = "disable_skill"
	InstanceCommandTypeQuarantineSkill     = "quarantine_skill"
	InstanceCommandTypeHandleSkillRisk     = "handle_skill_risk"
	InstanceCommandTypeSyncSkillInventory  = "sync_skill_inventory"
	InstanceCommandTypeRefreshSkillInventory = "refresh_skill_inventory"
	InstanceCommandTypeCollectSkillPackage = "collect_skill_package"
	instanceCommandStatusPending           = "pending"
	instanceCommandStatusDispatched        = "dispatched"
	instanceCommandStatusRunning           = "running"
	instanceCommandStatusSucceeded         = "succeeded"
	instanceCommandStatusFailed            = "failed"
)

type CreateInstanceCommandRequest struct {
	CommandType    string                 `json:"command_type"`
	Payload        map[string]interface{} `json:"payload,omitempty"`
	IdempotencyKey string                 `json:"idempotency_key"`
	TimeoutSeconds int                    `json:"timeout_seconds"`
}

type InstanceCommandPayload struct {
	ID             int                    `json:"id"`
	CommandType    string                 `json:"command_type"`
	Payload        map[string]interface{} `json:"payload,omitempty"`
	Status         string                 `json:"status"`
	IdempotencyKey string                 `json:"idempotency_key"`
	IssuedBy       *int                   `json:"issued_by,omitempty"`
	IssuedAt       time.Time              `json:"issued_at"`
	DispatchedAt   *time.Time             `json:"dispatched_at,omitempty"`
	StartedAt      *time.Time             `json:"started_at,omitempty"`
	FinishedAt     *time.Time             `json:"finished_at,omitempty"`
	TimeoutSeconds int                    `json:"timeout_seconds"`
	Result         map[string]interface{} `json:"result,omitempty"`
	ErrorMessage   *string                `json:"error_message,omitempty"`
}

type AgentCommandEnvelope struct {
	ID             int                    `json:"id"`
	CommandType    string                 `json:"command_type"`
	Payload        map[string]interface{} `json:"payload,omitempty"`
	IssuedAt       time.Time              `json:"issued_at"`
	TimeoutSeconds int                    `json:"timeout_seconds"`
	IdempotencyKey string                 `json:"idempotency_key"`
}

type AgentCommandFinishRequest struct {
	AgentID      string                 `json:"agent_id" binding:"required"`
	Status       string                 `json:"status" binding:"required"`
	FinishedAt   *time.Time             `json:"finished_at,omitempty"`
	Result       map[string]interface{} `json:"result,omitempty"`
	ErrorMessage string                 `json:"error_message"`
}

type InstanceCommandService interface {
	Create(instanceID int, issuedBy *int, req CreateInstanceCommandRequest) (*InstanceCommandPayload, error)
	GetNextForAgent(session *AgentSession) (*AgentCommandEnvelope, error)
	MarkStarted(session *AgentSession, commandID int, startedAt *time.Time) error
	MarkFinished(session *AgentSession, commandID int, req AgentCommandFinishRequest) error
	ListByInstanceID(instanceID int, limit int) ([]InstanceCommandPayload, error)
}

type instanceCommandService struct {
	commandRepo      repository.InstanceCommandRepository
	runtimeRepo      repository.InstanceRuntimeStatusRepository
	desiredStateRepo repository.InstanceDesiredStateRepository
}

func NewInstanceCommandService(commandRepo repository.InstanceCommandRepository, runtimeRepo repository.InstanceRuntimeStatusRepository, desiredStateRepo repository.InstanceDesiredStateRepository) InstanceCommandService {
	return &instanceCommandService{
		commandRepo:      commandRepo,
		runtimeRepo:      runtimeRepo,
		desiredStateRepo: desiredStateRepo,
	}
}

func (s *instanceCommandService) Create(instanceID int, issuedBy *int, req CreateInstanceCommandRequest) (*InstanceCommandPayload, error) {
	commandType := strings.TrimSpace(req.CommandType)
	if !isSupportedCommandType(commandType) {
		return nil, fmt.Errorf("invalid instance command type")
	}
	idempotencyKey := strings.TrimSpace(req.IdempotencyKey)
	if idempotencyKey == "" {
		idempotencyKey = fmt.Sprintf("%s-%d", commandType, time.Now().UTC().UnixNano())
	}

	existing, err := s.commandRepo.GetByInstanceIdempotencyKey(instanceID, idempotencyKey)
	if err != nil {
		return nil, err
	}
	if existing != nil {
		return commandPayloadFromModel(*existing)
	}

	payloadJSON, err := marshalOptionalJSON(req.Payload)
	if err != nil {
		return nil, fmt.Errorf("failed to encode command payload: %w", err)
	}
	timeoutSeconds := req.TimeoutSeconds
	if timeoutSeconds <= 0 {
		timeoutSeconds = 300
	}

	now := time.Now().UTC()
	command := &models.InstanceCommand{
		InstanceID:     instanceID,
		CommandType:    commandType,
		PayloadJSON:    payloadJSON,
		Status:         instanceCommandStatusPending,
		IdempotencyKey: idempotencyKey,
		IssuedBy:       issuedBy,
		IssuedAt:       now,
		TimeoutSeconds: timeoutSeconds,
		CreatedAt:      now,
		UpdatedAt:      now,
	}
	if err := s.commandRepo.Create(command); err != nil {
		return nil, err
	}

	if err := s.applyDesiredStateSideEffects(instanceID, commandType, req.Payload); err != nil {
		return nil, err
	}
	return commandPayloadFromModel(*command)
}

func (s *instanceCommandService) GetNextForAgent(session *AgentSession) (*AgentCommandEnvelope, error) {
	command, err := s.commandRepo.GetNextPendingByInstance(session.Instance.ID)
	if err != nil {
		return nil, err
	}
	if command == nil {
		return nil, nil
	}

	now := time.Now().UTC()
	command.Status = instanceCommandStatusDispatched
	command.AgentID = &session.Agent.AgentID
	command.DispatchedAt = &now
	if err := s.commandRepo.Update(command); err != nil {
		return nil, err
	}

	payload := map[string]interface{}{}
	if command.PayloadJSON != nil && strings.TrimSpace(*command.PayloadJSON) != "" {
		if err := json.Unmarshal([]byte(*command.PayloadJSON), &payload); err != nil {
			return nil, fmt.Errorf("failed to decode command payload: %w", err)
		}
	}

	return &AgentCommandEnvelope{
		ID:             command.ID,
		CommandType:    command.CommandType,
		Payload:        payload,
		IssuedAt:       command.IssuedAt,
		TimeoutSeconds: command.TimeoutSeconds,
		IdempotencyKey: command.IdempotencyKey,
	}, nil
}

func (s *instanceCommandService) MarkStarted(session *AgentSession, commandID int, startedAt *time.Time) error {
	command, err := s.commandRepo.GetByID(commandID)
	if err != nil {
		return err
	}
	if command == nil || command.InstanceID != session.Instance.ID {
		return fmt.Errorf("instance command not found")
	}

	if command.Status == instanceCommandStatusRunning || command.Status == instanceCommandStatusSucceeded || command.Status == instanceCommandStatusFailed {
		return nil
	}
	now := time.Now().UTC()
	if startedAt == nil || startedAt.IsZero() {
		startedAt = &now
	}
	command.Status = instanceCommandStatusRunning
	command.AgentID = &session.Agent.AgentID
	command.StartedAt = startedAt
	return s.commandRepo.Update(command)
}

func (s *instanceCommandService) MarkFinished(session *AgentSession, commandID int, req AgentCommandFinishRequest) error {
	command, err := s.commandRepo.GetByID(commandID)
	if err != nil {
		return err
	}
	if command == nil || command.InstanceID != session.Instance.ID {
		return fmt.Errorf("instance command not found")
	}
	if strings.TrimSpace(req.AgentID) != session.Agent.AgentID {
		return fmt.Errorf("agent id does not match session")
	}
	if req.Status != instanceCommandStatusSucceeded && req.Status != instanceCommandStatusFailed {
		return fmt.Errorf("invalid instance command finish status")
	}

	if command.Status == instanceCommandStatusSucceeded || command.Status == instanceCommandStatusFailed {
		return nil
	}

	finishedAt := req.FinishedAt
	if finishedAt == nil || finishedAt.IsZero() {
		now := time.Now().UTC()
		finishedAt = &now
	}
	command.Status = req.Status
	command.AgentID = &session.Agent.AgentID
	command.FinishedAt = finishedAt
	if req.Result != nil {
		resultJSON, err := marshalOptionalJSON(req.Result)
		if err != nil {
			return fmt.Errorf("failed to encode command result: %w", err)
		}
		command.ResultJSON = resultJSON
	}
	if strings.TrimSpace(req.ErrorMessage) != "" {
		command.ErrorMessage = optionalString(strings.TrimSpace(req.ErrorMessage))
	}
	return s.commandRepo.Update(command)
}

func (s *instanceCommandService) ListByInstanceID(instanceID int, limit int) ([]InstanceCommandPayload, error) {
	items, err := s.commandRepo.ListByInstanceID(instanceID, limit)
	if err != nil {
		return nil, err
	}
	result := make([]InstanceCommandPayload, 0, len(items))
	for _, item := range items {
		payload, err := commandPayloadFromModel(item)
		if err != nil {
			return nil, err
		}
		result = append(result, *payload)
	}
	return result, nil
}

func commandPayloadFromModel(item models.InstanceCommand) (*InstanceCommandPayload, error) {
	payload := &InstanceCommandPayload{
		ID:             item.ID,
		CommandType:    item.CommandType,
		Status:         item.Status,
		IdempotencyKey: item.IdempotencyKey,
		IssuedBy:       item.IssuedBy,
		IssuedAt:       item.IssuedAt,
		DispatchedAt:   item.DispatchedAt,
		StartedAt:      item.StartedAt,
		FinishedAt:     item.FinishedAt,
		TimeoutSeconds: item.TimeoutSeconds,
		ErrorMessage:   item.ErrorMessage,
	}
	if item.PayloadJSON != nil && strings.TrimSpace(*item.PayloadJSON) != "" {
		if err := json.Unmarshal([]byte(*item.PayloadJSON), &payload.Payload); err != nil {
			return nil, fmt.Errorf("failed to decode command payload: %w", err)
		}
	}
	if item.ResultJSON != nil && strings.TrimSpace(*item.ResultJSON) != "" {
		if err := json.Unmarshal([]byte(*item.ResultJSON), &payload.Result); err != nil {
			return nil, fmt.Errorf("failed to decode command result: %w", err)
		}
	}
	return payload, nil
}

func isSupportedCommandType(commandType string) bool {
	switch strings.TrimSpace(commandType) {
	case InstanceCommandTypeStartOpenClaw,
		InstanceCommandTypeStopOpenClaw,
		InstanceCommandTypeRestartOpenClaw,
		InstanceCommandTypeCollectSystemInfo,
		InstanceCommandTypeApplyConfigRevision,
		InstanceCommandTypeReloadConfig,
		InstanceCommandTypeHealthCheck,
		InstanceCommandTypeInstallSkill,
		InstanceCommandTypeUpdateSkill,
		InstanceCommandTypeUninstallSkill,
		InstanceCommandTypeRemoveSkill,
		InstanceCommandTypeDisableSkill,
		InstanceCommandTypeQuarantineSkill,
		InstanceCommandTypeHandleSkillRisk,
		InstanceCommandTypeSyncSkillInventory,
		InstanceCommandTypeRefreshSkillInventory,
		InstanceCommandTypeCollectSkillPackage:
		return true
	default:
		return false
	}
}

func (s *instanceCommandService) applyDesiredStateSideEffects(instanceID int, commandType string, payload map[string]interface{}) error {
	state, err := s.desiredStateRepo.GetByInstanceID(instanceID)
	if err != nil {
		return err
	}
	if state == nil {
		return nil
	}

	updated := false
	switch commandType {
	case InstanceCommandTypeStartOpenClaw, InstanceCommandTypeRestartOpenClaw:
		if state.DesiredPowerState != "running" {
			state.DesiredPowerState = "running"
			updated = true
		}
	case InstanceCommandTypeStopOpenClaw:
		if state.DesiredPowerState != "stopped" {
			state.DesiredPowerState = "stopped"
			updated = true
		}
	case InstanceCommandTypeApplyConfigRevision:
		if revisionID, ok := payload["revision_id"].(float64); ok {
			rev := int(revisionID)
			state.DesiredConfigRevisionID = &rev
			updated = true
		}
	}
	if !updated {
		return nil
	}

	state.UpdatedAt = time.Now().UTC()
	if err := s.desiredStateRepo.Update(state); err != nil {
		return err
	}

	runtime, err := s.runtimeRepo.GetByInstanceID(instanceID)
	if err == nil && runtime != nil {
		runtime.DesiredConfigRevisionID = state.DesiredConfigRevisionID
		_ = s.runtimeRepo.Update(runtime)
	}
	return nil
}
