package service

import (
	"context"
	"net/http"
	"strings"
	"time"

	"cmr-backend/internal/apperr"
	"cmr-backend/internal/platform/security"
	"cmr-backend/internal/store/postgres"
)

const (
	GuestLaunchSource     = "public-default-experience"
	GuestIdentityProvider = "guest_device"
	GuestIdentityType     = "guest"
)

type PublicExperienceService struct {
	store        *postgres.Store
	mapService   *MapExperienceService
	eventService *EventService
}

type PublicEventPlayInput struct {
	EventPublicID string
}

type PublicLaunchEventInput struct {
	EventPublicID string `json:"-"`
	ReleaseID     string `json:"releaseId,omitempty"`
	VariantID     string `json:"variantId,omitempty"`
	ClientType    string `json:"clientType"`
	DeviceKey     string `json:"deviceKey"`
}

func NewPublicExperienceService(store *postgres.Store, mapService *MapExperienceService, eventService *EventService) *PublicExperienceService {
	return &PublicExperienceService{
		store:        store,
		mapService:   mapService,
		eventService: eventService,
	}
}

func (s *PublicExperienceService) ListMaps(ctx context.Context, input ListExperienceMapsInput) ([]ExperienceMapSummary, error) {
	return s.mapService.ListMaps(ctx, input)
}

func (s *PublicExperienceService) GetMapDetail(ctx context.Context, mapPublicID string) (*ExperienceMapDetail, error) {
	return s.mapService.GetMapDetail(ctx, mapPublicID)
}

func (s *PublicExperienceService) GetEventDetail(ctx context.Context, eventPublicID string) (*EventDetailResult, error) {
	event, err := s.store.GetEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if err := ensurePublicExperienceEvent(event); err != nil {
		return nil, err
	}
	return s.eventService.GetEventDetail(ctx, eventPublicID)
}

func (s *PublicExperienceService) GetEventPlay(ctx context.Context, input PublicEventPlayInput) (*EventPlayResult, error) {
	input.EventPublicID = strings.TrimSpace(input.EventPublicID)
	if input.EventPublicID == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id is required")
	}

	event, err := s.store.GetEventByPublicID(ctx, input.EventPublicID)
	if err != nil {
		return nil, err
	}
	if err := ensurePublicExperienceEvent(event); err != nil {
		return nil, err
	}

	result := &EventPlayResult{}
	result.Event.ID = event.PublicID
	result.Event.Slug = event.Slug
	result.Event.DisplayName = event.DisplayName
	result.Event.Summary = event.Summary
	result.Event.Status = event.Status
	variantPlan := resolveVariantPlan(event.ReleasePayloadJSON)
	result.Play.AssignmentMode = variantPlan.AssignmentMode
	if len(variantPlan.CourseVariants) > 0 {
		result.Play.CourseVariants = variantPlan.CourseVariants
	}
	if event.CurrentReleasePubID != nil && event.ConfigLabel != nil && event.ManifestURL != nil {
		result.Release = &struct {
			ID                     string  `json:"id"`
			ConfigLabel            string  `json:"configLabel"`
			ManifestURL            string  `json:"manifestUrl"`
			ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
			RouteCode              *string `json:"routeCode,omitempty"`
		}{
			ID:                     *event.CurrentReleasePubID,
			ConfigLabel:            *event.ConfigLabel,
			ManifestURL:            *event.ManifestURL,
			ManifestChecksumSha256: event.ManifestChecksum,
			RouteCode:              event.RouteCode,
		}
	}
	result.ResolvedRelease = buildResolvedReleaseFromEvent(event, GuestLaunchSource)
	result.Runtime = buildRuntimeSummaryFromEvent(event)
	if preview, err := buildPreviewFromPayload(event.ReleasePayloadJSON); err != nil {
		return nil, err
	} else {
		result.Preview = preview
	}
	result.CurrentPresentation = buildPresentationSummaryFromEvent(event)
	if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
		return nil, err
	} else if enrichedPresentation != nil {
		result.CurrentPresentation = enrichedPresentation
	}
	result.CurrentContentBundle = buildContentBundleSummaryFromEvent(event)
	if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
		return nil, err
	} else if enrichedBundle != nil {
		result.CurrentContentBundle = enrichedBundle
	}

	canLaunch, launchReason := evaluateEventLaunchReadiness(event)
	result.Play.CanLaunch = canLaunch
	if canLaunch {
		result.Play.LaunchSource = GuestLaunchSource
		result.Play.PrimaryAction = "start"
		result.Play.Reason = "guest can start default experience"
		return result, nil
	}
	result.Play.PrimaryAction = "unavailable"
	result.Play.Reason = launchReason
	return result, nil
}

func (s *PublicExperienceService) LaunchEvent(ctx context.Context, input PublicLaunchEventInput) (*LaunchEventResult, error) {
	input.EventPublicID = strings.TrimSpace(input.EventPublicID)
	input.ReleaseID = strings.TrimSpace(input.ReleaseID)
	input.VariantID = strings.TrimSpace(input.VariantID)
	input.DeviceKey = strings.TrimSpace(input.DeviceKey)
	if input.EventPublicID == "" || input.DeviceKey == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id and deviceKey are required")
	}
	if err := validateClientType(input.ClientType); err != nil {
		return nil, err
	}

	event, err := s.store.GetEventByPublicID(ctx, input.EventPublicID)
	if err != nil {
		return nil, err
	}
	if err := ensurePublicExperienceEvent(event); err != nil {
		return nil, err
	}
	if canLaunch, reason := evaluateEventLaunchReadiness(event); !canLaunch {
		return nil, launchReadinessError(reason)
	}
	if input.ReleaseID != "" && event.CurrentReleasePubID != nil && input.ReleaseID != *event.CurrentReleasePubID {
		return nil, apperr.New(http.StatusConflict, "release_not_launchable", "requested release is not the current published release")
	}

	variantPlan := resolveVariantPlan(event.ReleasePayloadJSON)
	variant, err := resolveLaunchVariant(variantPlan, input.VariantID)
	if err != nil {
		return nil, err
	}
	routeCode := event.RouteCode
	var assignmentMode *string
	var variantID *string
	var variantName *string
	if variant != nil {
		resultMode := variant.AssignmentMode
		assignmentMode = &resultMode
		variantID = &variant.ID
		variantName = &variant.Name
		if variant.RouteCode != nil {
			routeCode = variant.RouteCode
		}
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	guestUser, err := s.findOrCreateGuestUser(ctx, tx, input.ClientType, input.DeviceKey)
	if err != nil {
		return nil, err
	}
	if err := s.store.TouchUserLogin(ctx, tx, guestUser.ID); err != nil {
		return nil, err
	}

	sessionPublicID, err := security.GeneratePublicID("sess")
	if err != nil {
		return nil, err
	}
	sessionToken, err := security.GenerateToken(32)
	if err != nil {
		return nil, err
	}
	sessionTokenExpiresAt := time.Now().UTC().Add(2 * time.Hour)

	session, err := s.store.CreateGameSession(ctx, tx, postgres.CreateGameSessionParams{
		SessionPublicID:       sessionPublicID,
		UserID:                guestUser.ID,
		EventID:               event.ID,
		EventReleaseID:        *event.CurrentReleaseID,
		DeviceKey:             input.DeviceKey,
		ClientType:            input.ClientType,
		AssignmentMode:        assignmentMode,
		VariantID:             variantID,
		VariantName:           variantName,
		RouteCode:             routeCode,
		SessionTokenHash:      security.HashText(sessionToken),
		SessionTokenExpiresAt: sessionTokenExpiresAt,
	})
	if err != nil {
		return nil, err
	}

	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	result := &LaunchEventResult{}
	result.Event.ID = event.PublicID
	result.Event.DisplayName = event.DisplayName
	result.Launch.Source = GuestLaunchSource
	result.Launch.ResolvedRelease = buildResolvedReleaseFromEvent(event, GuestLaunchSource)
	result.Launch.Variant = variant
	result.Launch.Runtime = buildRuntimeSummaryFromEvent(event)
	result.Launch.Presentation = buildPresentationSummaryFromEvent(event)
	if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
		return nil, err
	} else if enrichedPresentation != nil {
		result.Launch.Presentation = enrichedPresentation
	}
	result.Launch.ContentBundle = buildContentBundleSummaryFromEvent(event)
	if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
		return nil, err
	} else if enrichedBundle != nil {
		result.Launch.ContentBundle = enrichedBundle
	}
	result.Launch.Config.ConfigURL = *event.ManifestURL
	result.Launch.Config.ConfigLabel = *event.ConfigLabel
	result.Launch.Config.ConfigChecksumSha256 = event.ManifestChecksum
	result.Launch.Config.ReleaseID = *event.CurrentReleasePubID
	result.Launch.Config.RouteCode = routeCode
	result.Launch.Business.Source = GuestLaunchSource
	result.Launch.Business.EventID = event.PublicID
	result.Launch.Business.SessionID = session.SessionPublicID
	result.Launch.Business.SessionToken = sessionToken
	result.Launch.Business.SessionTokenExpiresAt = session.SessionTokenExpiresAt.Format(time.RFC3339)
	result.Launch.Business.RouteCode = routeCode
	result.Launch.Business.IsGuest = true
	return result, nil
}

func (s *PublicExperienceService) findOrCreateGuestUser(ctx context.Context, tx postgres.Tx, clientType, deviceKey string) (*postgres.User, error) {
	providerSubject := clientType + ":" + deviceKey
	user, err := s.store.FindUserByProviderSubject(ctx, tx, GuestIdentityProvider, providerSubject)
	if err != nil {
		return nil, err
	}
	if user != nil {
		return user, nil
	}
	userPublicID, err := security.GeneratePublicID("usr")
	if err != nil {
		return nil, err
	}
	user, err = s.store.CreateUser(ctx, tx, postgres.CreateUserParams{
		PublicID: userPublicID,
		Status:   "active",
	})
	if err != nil {
		return nil, err
	}
	if err := s.store.CreateIdentity(ctx, tx, postgres.CreateIdentityParams{
		UserID:       user.ID,
		IdentityType: GuestIdentityType,
		Provider:     GuestIdentityProvider,
		ProviderSubj: providerSubject,
		ProfileJSON:  "{}",
	}); err != nil {
		return nil, err
	}
	return user, nil
}

func ensurePublicExperienceEvent(event *postgres.Event) error {
	if event == nil {
		return apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}
	if !event.IsDefaultExperience {
		return apperr.New(http.StatusForbidden, "event_not_public", "event is not available in guest mode")
	}
	return nil
}
