package postgres

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/jackc/pgx/v5"
)

type SessionResult struct {
	ID                string
	SessionID         string
	ResultStatus      string
	SummaryJSON       string
	FinalDurationSec  *int
	FinalScore        *int
	CompletedControls *int
	TotalControls     *int
	DistanceMeters    *float64
	AverageSpeedKmh   *float64
	MaxHeartRateBpm   *int
}

type UpsertSessionResultParams struct {
	SessionID         string
	ResultStatus      string
	Summary           map[string]any
	FinalDurationSec  *int
	FinalScore        *int
	CompletedControls *int
	TotalControls     *int
	DistanceMeters    *float64
	AverageSpeedKmh   *float64
	MaxHeartRateBpm   *int
}

type SessionResultRecord struct {
	Session
	Result *SessionResult
}

func (s *Store) UpsertSessionResult(ctx context.Context, tx Tx, params UpsertSessionResultParams) (*SessionResult, error) {
	summaryJSON, err := json.Marshal(params.Summary)
	if err != nil {
		return nil, fmt.Errorf("marshal session summary: %w", err)
	}

	row := tx.QueryRow(ctx, `
		INSERT INTO session_results (
			session_id,
			result_status,
			summary_jsonb,
			final_duration_sec,
			final_score,
			completed_controls,
			total_controls,
			distance_meters,
			average_speed_kmh,
			max_heart_rate_bpm
		)
		VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7, $8, $9, $10)
		ON CONFLICT (session_id) DO UPDATE SET
			result_status = EXCLUDED.result_status,
			summary_jsonb = EXCLUDED.summary_jsonb,
			final_duration_sec = EXCLUDED.final_duration_sec,
			final_score = EXCLUDED.final_score,
			completed_controls = EXCLUDED.completed_controls,
			total_controls = EXCLUDED.total_controls,
			distance_meters = EXCLUDED.distance_meters,
			average_speed_kmh = EXCLUDED.average_speed_kmh,
			max_heart_rate_bpm = EXCLUDED.max_heart_rate_bpm
		RETURNING
			id,
			session_id,
			result_status,
			summary_jsonb::text,
			final_duration_sec,
			final_score,
			completed_controls,
			total_controls,
			distance_meters::float8,
			average_speed_kmh::float8,
			max_heart_rate_bpm
	`, params.SessionID, params.ResultStatus, string(summaryJSON), params.FinalDurationSec, params.FinalScore, params.CompletedControls, params.TotalControls, params.DistanceMeters, params.AverageSpeedKmh, params.MaxHeartRateBpm)

	return scanSessionResult(row)
}

func (s *Store) GetSessionResultByPublicID(ctx context.Context, sessionPublicID string) (*SessionResultRecord, error) {
	row := s.pool.QueryRow(ctx, `
		SELECT
			gs.id,
			gs.session_public_id,
			gs.user_id,
			gs.event_id,
			gs.event_release_id,
			er.release_public_id,
			er.config_label,
			er.manifest_url,
			er.manifest_checksum_sha256,
			gs.device_key,
			gs.client_type,
			gs.assignment_mode,
			gs.variant_id,
			gs.variant_name,
			gs.route_code,
			gs.status,
			gs.session_token_hash,
			gs.session_token_expires_at,
			gs.launched_at,
			gs.started_at,
			gs.ended_at,
			e.event_public_id,
			e.display_name,
			sr.id,
			sr.session_id,
			sr.result_status,
			sr.summary_jsonb::text,
			sr.final_duration_sec,
			sr.final_score,
			sr.completed_controls,
			sr.total_controls,
			sr.distance_meters::float8,
			sr.average_speed_kmh::float8,
			sr.max_heart_rate_bpm
		FROM game_sessions gs
		JOIN events e ON e.id = gs.event_id
		JOIN event_releases er ON er.id = gs.event_release_id
		LEFT JOIN session_results sr ON sr.session_id = gs.id
		WHERE gs.session_public_id = $1
		LIMIT 1
	`, sessionPublicID)
	return scanSessionResultRecord(row)
}

func (s *Store) ListSessionResultsByUserID(ctx context.Context, userID string, limit int) ([]SessionResultRecord, error) {
	if limit <= 0 || limit > 100 {
		limit = 20
	}

	rows, err := s.pool.Query(ctx, `
		SELECT
			gs.id,
			gs.session_public_id,
			gs.user_id,
			gs.event_id,
			gs.event_release_id,
			er.release_public_id,
			er.config_label,
			er.manifest_url,
			er.manifest_checksum_sha256,
			gs.device_key,
			gs.client_type,
			gs.assignment_mode,
			gs.variant_id,
			gs.variant_name,
			gs.route_code,
			gs.status,
			gs.session_token_hash,
			gs.session_token_expires_at,
			gs.launched_at,
			gs.started_at,
			gs.ended_at,
			e.event_public_id,
			e.display_name,
			sr.id,
			sr.session_id,
			sr.result_status,
			sr.summary_jsonb::text,
			sr.final_duration_sec,
			sr.final_score,
			sr.completed_controls,
			sr.total_controls,
			sr.distance_meters::float8,
			sr.average_speed_kmh::float8,
			sr.max_heart_rate_bpm
		FROM game_sessions gs
		JOIN events e ON e.id = gs.event_id
		JOIN event_releases er ON er.id = gs.event_release_id
		LEFT JOIN session_results sr ON sr.session_id = gs.id
		WHERE gs.user_id = $1
		  AND gs.status IN ('finished', 'failed', 'cancelled')
		ORDER BY COALESCE(gs.ended_at, gs.updated_at, gs.created_at) DESC
		LIMIT $2
	`, userID, limit)
	if err != nil {
		return nil, fmt.Errorf("list session results by user id: %w", err)
	}
	defer rows.Close()

	var items []SessionResultRecord
	for rows.Next() {
		item, err := scanSessionResultRecordFromRows(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, *item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate session results by user id: %w", err)
	}
	return items, nil
}

func scanSessionResult(row pgx.Row) (*SessionResult, error) {
	var result SessionResult
	err := row.Scan(
		&result.ID,
		&result.SessionID,
		&result.ResultStatus,
		&result.SummaryJSON,
		&result.FinalDurationSec,
		&result.FinalScore,
		&result.CompletedControls,
		&result.TotalControls,
		&result.DistanceMeters,
		&result.AverageSpeedKmh,
		&result.MaxHeartRateBpm,
	)
	if err != nil {
		return nil, fmt.Errorf("scan session result: %w", err)
	}
	return &result, nil
}

func scanSessionResultRecord(row pgx.Row) (*SessionResultRecord, error) {
	var record SessionResultRecord
	var resultID *string
	var resultSessionID *string
	var resultStatus *string
	var resultSummaryJSON *string
	var finalDurationSec *int
	var finalScore *int
	var completedControls *int
	var totalControls *int
	var distanceMeters *float64
	var averageSpeedKmh *float64
	var maxHeartRateBpm *int

	err := row.Scan(
		&record.ID,
		&record.SessionPublicID,
		&record.UserID,
		&record.EventID,
		&record.EventReleaseID,
		&record.ReleasePublicID,
		&record.ConfigLabel,
		&record.ManifestURL,
		&record.ManifestChecksum,
		&record.DeviceKey,
		&record.ClientType,
		&record.AssignmentMode,
		&record.VariantID,
		&record.VariantName,
		&record.RouteCode,
		&record.Status,
		&record.SessionTokenHash,
		&record.SessionTokenExpiresAt,
		&record.LaunchedAt,
		&record.StartedAt,
		&record.EndedAt,
		&record.EventPublicID,
		&record.EventDisplayName,
		&resultID,
		&resultSessionID,
		&resultStatus,
		&resultSummaryJSON,
		&finalDurationSec,
		&finalScore,
		&completedControls,
		&totalControls,
		&distanceMeters,
		&averageSpeedKmh,
		&maxHeartRateBpm,
	)
	if err != nil {
		if err == pgx.ErrNoRows {
			return nil, nil
		}
		return nil, fmt.Errorf("scan session result record: %w", err)
	}

	if resultID != nil {
		record.Result = &SessionResult{
			ID:                *resultID,
			SessionID:         derefString(resultSessionID),
			ResultStatus:      derefString(resultStatus),
			SummaryJSON:       derefString(resultSummaryJSON),
			FinalDurationSec:  finalDurationSec,
			FinalScore:        finalScore,
			CompletedControls: completedControls,
			TotalControls:     totalControls,
			DistanceMeters:    distanceMeters,
			AverageSpeedKmh:   averageSpeedKmh,
			MaxHeartRateBpm:   maxHeartRateBpm,
		}
	}

	return &record, nil
}

func scanSessionResultRecordFromRows(rows pgx.Rows) (*SessionResultRecord, error) {
	var record SessionResultRecord
	var resultID *string
	var resultSessionID *string
	var resultStatus *string
	var resultSummaryJSON *string
	var finalDurationSec *int
	var finalScore *int
	var completedControls *int
	var totalControls *int
	var distanceMeters *float64
	var averageSpeedKmh *float64
	var maxHeartRateBpm *int

	err := rows.Scan(
		&record.ID,
		&record.SessionPublicID,
		&record.UserID,
		&record.EventID,
		&record.EventReleaseID,
		&record.ReleasePublicID,
		&record.ConfigLabel,
		&record.ManifestURL,
		&record.ManifestChecksum,
		&record.DeviceKey,
		&record.ClientType,
		&record.AssignmentMode,
		&record.VariantID,
		&record.VariantName,
		&record.RouteCode,
		&record.Status,
		&record.SessionTokenHash,
		&record.SessionTokenExpiresAt,
		&record.LaunchedAt,
		&record.StartedAt,
		&record.EndedAt,
		&record.EventPublicID,
		&record.EventDisplayName,
		&resultID,
		&resultSessionID,
		&resultStatus,
		&resultSummaryJSON,
		&finalDurationSec,
		&finalScore,
		&completedControls,
		&totalControls,
		&distanceMeters,
		&averageSpeedKmh,
		&maxHeartRateBpm,
	)
	if err != nil {
		return nil, fmt.Errorf("scan session result row: %w", err)
	}
	if resultID != nil {
		record.Result = &SessionResult{
			ID:                *resultID,
			SessionID:         derefString(resultSessionID),
			ResultStatus:      derefString(resultStatus),
			SummaryJSON:       derefString(resultSummaryJSON),
			FinalDurationSec:  finalDurationSec,
			FinalScore:        finalScore,
			CompletedControls: completedControls,
			TotalControls:     totalControls,
			DistanceMeters:    distanceMeters,
			AverageSpeedKmh:   averageSpeedKmh,
			MaxHeartRateBpm:   maxHeartRateBpm,
		}
	}
	return &record, nil
}

func derefString(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}
