package service

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

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

type SessionService struct {
	store *postgres.Store
}

type sessionTokenPolicy struct {
	AllowExpired bool
}

type SessionResult struct {
	Session struct {
		ID                    string  `json:"id"`
		Status                string  `json:"status"`
		ClientType            string  `json:"clientType"`
		DeviceKey             string  `json:"deviceKey"`
		AssignmentMode        *string `json:"assignmentMode,omitempty"`
		VariantID             *string `json:"variantId,omitempty"`
		VariantName           *string `json:"variantName,omitempty"`
		RouteCode             *string `json:"routeCode,omitempty"`
		SessionTokenExpiresAt string  `json:"sessionTokenExpiresAt"`
		LaunchedAt            string  `json:"launchedAt"`
		StartedAt             *string `json:"startedAt,omitempty"`
		EndedAt               *string `json:"endedAt,omitempty"`
	} `json:"session"`
	Event struct {
		ID          string `json:"id"`
		DisplayName string `json:"displayName"`
	} `json:"event"`
	ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
}

type SessionActionInput struct {
	SessionPublicID string
	SessionToken    string `json:"sessionToken"`
}

type FinishSessionInput struct {
	SessionPublicID string
	SessionToken    string               `json:"sessionToken"`
	Status          string               `json:"status"`
	Summary         *SessionSummaryInput `json:"summary,omitempty"`
}

type SessionSummaryInput struct {
	FinalDurationSec  *int     `json:"finalDurationSec,omitempty"`
	FinalScore        *int     `json:"finalScore,omitempty"`
	CompletedControls *int     `json:"completedControls,omitempty"`
	TotalControls     *int     `json:"totalControls,omitempty"`
	DistanceMeters    *float64 `json:"distanceMeters,omitempty"`
	AverageSpeedKmh   *float64 `json:"averageSpeedKmh,omitempty"`
	MaxHeartRateBpm   *int     `json:"maxHeartRateBpm,omitempty"`
}

func NewSessionService(store *postgres.Store) *SessionService {
	return &SessionService{store: store}
}

func (s *SessionService) GetSession(ctx context.Context, sessionPublicID, userID string) (*SessionResult, error) {
	sessionPublicID = strings.TrimSpace(sessionPublicID)
	if sessionPublicID == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "session id is required")
	}

	session, err := s.store.GetSessionByPublicID(ctx, sessionPublicID)
	if err != nil {
		return nil, err
	}
	if session == nil {
		return nil, apperr.New(http.StatusNotFound, "session_not_found", "session not found")
	}
	if userID != "" && session.UserID != userID {
		return nil, apperr.New(http.StatusForbidden, "session_forbidden", "session does not belong to current user")
	}

	return buildSessionResult(session), nil
}

func (s *SessionService) ListMySessions(ctx context.Context, userID string, limit int) ([]SessionResult, error) {
	if userID == "" {
		return nil, apperr.New(http.StatusUnauthorized, "unauthorized", "user is required")
	}

	sessions, err := s.store.ListSessionsByUserID(ctx, userID, limit)
	if err != nil {
		return nil, err
	}

	results := make([]SessionResult, 0, len(sessions))
	for i := range sessions {
		results = append(results, *buildSessionResult(&sessions[i]))
	}
	return results, nil
}

func (s *SessionService) StartSession(ctx context.Context, input SessionActionInput) (*SessionResult, error) {
	session, err := s.validateSessionAction(ctx, input.SessionPublicID, input.SessionToken, sessionTokenPolicy{})
	if err != nil {
		return nil, err
	}
	if session.Status == SessionStatusRunning || isSessionTerminalStatus(session.Status) {
		return buildSessionResult(session), nil
	}

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

	locked, err := s.store.GetSessionByPublicIDForUpdate(ctx, tx, input.SessionPublicID)
	if err != nil {
		return nil, err
	}
	if locked == nil {
		return nil, apperr.New(http.StatusNotFound, "session_not_found", "session not found")
	}
	if err := s.verifySessionToken(locked, input.SessionToken, sessionTokenPolicy{}); err != nil {
		return nil, err
	}
	if locked.Status == SessionStatusRunning || isSessionTerminalStatus(locked.Status) {
		if err := tx.Commit(ctx); err != nil {
			return nil, err
		}
		return buildSessionResult(locked), nil
	}

	if locked.Status == SessionStatusLaunched {
		if err := s.store.StartSession(ctx, tx, locked.ID); err != nil {
			return nil, err
		}
	}

	updated, err := s.store.GetSessionByPublicIDForUpdate(ctx, tx, input.SessionPublicID)
	if err != nil {
		return nil, err
	}
	if updated == nil {
		return nil, apperr.New(http.StatusNotFound, "session_not_found", "session not found")
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	return buildSessionResult(updated), nil
}

func (s *SessionService) FinishSession(ctx context.Context, input FinishSessionInput) (*SessionResult, error) {
	status, err := normalizeFinishStatus(input.Status)
	if err != nil {
		return nil, err
	}
	input.Status = status

	session, err := s.validateSessionAction(ctx, input.SessionPublicID, input.SessionToken, sessionTokenPolicy{
		AllowExpired: input.Status == SessionStatusCancelled,
	})
	if err != nil {
		return nil, err
	}

	if isSessionTerminalStatus(session.Status) {
		return buildSessionResult(session), nil
	}

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

	locked, err := s.store.GetSessionByPublicIDForUpdate(ctx, tx, input.SessionPublicID)
	if err != nil {
		return nil, err
	}
	if locked == nil {
		return nil, apperr.New(http.StatusNotFound, "session_not_found", "session not found")
	}
	if err := s.verifySessionToken(locked, input.SessionToken, sessionTokenPolicy{
		AllowExpired: input.Status == SessionStatusCancelled || isSessionTerminalStatus(locked.Status),
	}); err != nil {
		return nil, err
	}

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

	if err := s.store.FinishSession(ctx, tx, postgres.FinishSessionParams{
		SessionID: locked.ID,
		Status:    input.Status,
	}); err != nil {
		return nil, err
	}

	updated, err := s.store.GetSessionByPublicIDForUpdate(ctx, tx, input.SessionPublicID)
	if err != nil {
		return nil, err
	}
	if _, err := s.store.UpsertSessionResult(ctx, tx, postgres.UpsertSessionResultParams{
		SessionID:         updated.ID,
		ResultStatus:      input.Status,
		Summary:           buildSummaryMap(input.Summary),
		FinalDurationSec:  resolveDurationSeconds(updated, input.Summary),
		FinalScore:        summaryInt(input.Summary, func(v *SessionSummaryInput) *int { return v.FinalScore }),
		CompletedControls: summaryInt(input.Summary, func(v *SessionSummaryInput) *int { return v.CompletedControls }),
		TotalControls:     summaryInt(input.Summary, func(v *SessionSummaryInput) *int { return v.TotalControls }),
		DistanceMeters:    summaryFloat(input.Summary, func(v *SessionSummaryInput) *float64 { return v.DistanceMeters }),
		AverageSpeedKmh:   summaryFloat(input.Summary, func(v *SessionSummaryInput) *float64 { return v.AverageSpeedKmh }),
		MaxHeartRateBpm:   summaryInt(input.Summary, func(v *SessionSummaryInput) *int { return v.MaxHeartRateBpm }),
	}); err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	return buildSessionResult(updated), nil
}

func (s *SessionService) validateSessionAction(ctx context.Context, sessionPublicID, sessionToken string, policy sessionTokenPolicy) (*postgres.Session, error) {
	sessionPublicID = strings.TrimSpace(sessionPublicID)
	sessionToken = strings.TrimSpace(sessionToken)
	if sessionPublicID == "" || sessionToken == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "session id and sessionToken are required")
	}

	session, err := s.store.GetSessionByPublicID(ctx, sessionPublicID)
	if err != nil {
		return nil, err
	}
	if session == nil {
		return nil, apperr.New(http.StatusNotFound, "session_not_found", "session not found")
	}
	if err := s.verifySessionToken(session, sessionToken, policy); err != nil {
		return nil, err
	}
	return session, nil
}

func (s *SessionService) verifySessionToken(session *postgres.Session, sessionToken string, policy sessionTokenPolicy) error {
	if session.SessionTokenHash != security.HashText(sessionToken) {
		return apperr.New(http.StatusUnauthorized, "invalid_session_token", "invalid session token")
	}
	if !policy.AllowExpired && session.SessionTokenExpiresAt.Before(time.Now().UTC()) {
		return apperr.New(http.StatusUnauthorized, "session_token_expired", "session token expired")
	}
	return nil
}

func buildSessionResult(session *postgres.Session) *SessionResult {
	result := &SessionResult{}
	result.Session.ID = session.SessionPublicID
	result.Session.Status = session.Status
	result.Session.ClientType = session.ClientType
	result.Session.DeviceKey = session.DeviceKey
	result.Session.AssignmentMode = session.AssignmentMode
	result.Session.VariantID = session.VariantID
	result.Session.VariantName = session.VariantName
	result.Session.RouteCode = session.RouteCode
	result.Session.SessionTokenExpiresAt = session.SessionTokenExpiresAt.Format(time.RFC3339)
	result.Session.LaunchedAt = session.LaunchedAt.Format(time.RFC3339)
	if session.StartedAt != nil {
		value := session.StartedAt.Format(time.RFC3339)
		result.Session.StartedAt = &value
	}
	if session.EndedAt != nil {
		value := session.EndedAt.Format(time.RFC3339)
		result.Session.EndedAt = &value
	}
	if session.EventPublicID != nil {
		result.Event.ID = *session.EventPublicID
	}
	if session.EventDisplayName != nil {
		result.Event.DisplayName = *session.EventDisplayName
	}
	result.ResolvedRelease = buildResolvedReleaseFromSession(session, LaunchSourceEventCurrentRelease)
	return result
}

func normalizeFinishStatus(value string) (string, error) {
	switch strings.TrimSpace(value) {
	case "", SessionStatusFinished:
		return SessionStatusFinished, nil
	case SessionStatusFailed:
		return SessionStatusFailed, nil
	case SessionStatusCancelled:
		return SessionStatusCancelled, nil
	default:
		return "", apperr.New(http.StatusBadRequest, "invalid_finish_status", "status must be finished, failed or cancelled")
	}
}

func buildSummaryMap(summary *SessionSummaryInput) map[string]any {
	if summary == nil {
		return map[string]any{}
	}
	raw, err := json.Marshal(summary)
	if err != nil {
		return map[string]any{}
	}
	result := map[string]any{}
	if err := json.Unmarshal(raw, &result); err != nil {
		return map[string]any{}
	}
	return result
}

func resolveDurationSeconds(session *postgres.Session, summary *SessionSummaryInput) *int {
	if summary != nil && summary.FinalDurationSec != nil {
		return summary.FinalDurationSec
	}
	if session.StartedAt != nil {
		endAt := time.Now().UTC()
		if session.EndedAt != nil {
			endAt = *session.EndedAt
		}
		seconds := int(endAt.Sub(*session.StartedAt).Seconds())
		if seconds < 0 {
			seconds = 0
		}
		return &seconds
	}
	return nil
}

func summaryInt(summary *SessionSummaryInput, getter func(*SessionSummaryInput) *int) *int {
	if summary == nil {
		return nil
	}
	return getter(summary)
}

func summaryFloat(summary *SessionSummaryInput, getter func(*SessionSummaryInput) *float64) *float64 {
	if summary == nil {
		return nil
	}
	return getter(summary)
}
