package main

import (
	"context"
	_ "embed"
	"fmt"
	"time"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
	"go.opentelemetry.io/otel"
)

//go:embed migrations/001_init.sql
var migrationSQL string

type store interface {
	Ping(context.Context) error
	Migrate(context.Context) error
	ListBuoys(context.Context) ([]buoy, error)
	CreateBuoy(context.Context, buoy) (buoy, error)
	GetBuoy(context.Context, string) (buoy, error)
	DeleteBuoy(context.Context, string) error
	ListReadings(context.Context, string, *time.Time, *time.Time, int, int) ([]reading, error)
	CreateReading(context.Context, reading) (reading, error)
	AggregateReadings(context.Context, time.Duration) ([]aggregate, error)
	Close()
}

type postgresStore struct {
	pool    *pgxpool.Pool
	metrics *metrics
}

func newPostgresStore(ctx context.Context, dsn string, m *metrics) (*postgresStore, error) {
	pool, err := pgxpool.New(ctx, dsn)
	if err != nil {
		return nil, fmt.Errorf("create database pool: %w", err)
	}
	return &postgresStore{pool: pool, metrics: m}, nil
}

func (s *postgresStore) observe(ctx context.Context, operation string) (context.Context, func()) {
	ctx, span := otel.Tracer("nereus-api/database").Start(ctx, operation)
	started := time.Now()
	return ctx, func() {
		s.metrics.db.WithLabelValues(operation).Observe(time.Since(started).Seconds())
		span.End()
	}
}

func (s *postgresStore) Ping(ctx context.Context) error { return s.pool.Ping(ctx) }
func (s *postgresStore) Close()                         { s.pool.Close() }

func (s *postgresStore) Migrate(ctx context.Context) error {
	ctx, done := s.observe(ctx, "migrate")
	defer done()
	if _, err := s.pool.Exec(ctx, migrationSQL); err != nil {
		return fmt.Errorf("apply migrations: %w", err)
	}
	return nil
}

func (s *postgresStore) ListBuoys(ctx context.Context) ([]buoy, error) {
	ctx, done := s.observe(ctx, "list_buoys")
	defer done()
	rows, err := s.pool.Query(ctx, `SELECT id, name, latitude, longitude, created_at FROM buoys ORDER BY created_at`)
	if err != nil {
		return nil, fmt.Errorf("query buoys: %w", err)
	}
	defer rows.Close()
	items, err := pgx.CollectRows(rows, pgx.RowToStructByName[buoy])
	if err != nil {
		return nil, fmt.Errorf("collect buoys: %w", err)
	}
	return items, nil
}

func (s *postgresStore) CreateBuoy(ctx context.Context, b buoy) (buoy, error) {
	ctx, done := s.observe(ctx, "create_buoy")
	defer done()
	err := s.pool.QueryRow(ctx, `INSERT INTO buoys (id,name,latitude,longitude) VALUES ($1,$2,$3,$4) RETURNING created_at`, b.ID, b.Name, b.Latitude, b.Longitude).Scan(&b.CreatedAt)
	if err != nil {
		return buoy{}, fmt.Errorf("insert buoy: %w", err)
	}
	return b, nil
}

func (s *postgresStore) GetBuoy(ctx context.Context, id string) (buoy, error) {
	ctx, done := s.observe(ctx, "get_buoy")
	defer done()
	var b buoy
	err := s.pool.QueryRow(ctx, `SELECT id, name, latitude, longitude, created_at FROM buoys WHERE id=$1`, id).Scan(&b.ID, &b.Name, &b.Latitude, &b.Longitude, &b.CreatedAt)
	if err != nil {
		return buoy{}, fmt.Errorf("select buoy: %w", err)
	}
	return b, nil
}

func (s *postgresStore) DeleteBuoy(ctx context.Context, id string) error {
	ctx, done := s.observe(ctx, "delete_buoy")
	defer done()
	tag, err := s.pool.Exec(ctx, `DELETE FROM buoys WHERE id=$1`, id)
	if err != nil {
		return fmt.Errorf("delete buoy: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return pgx.ErrNoRows
	}
	return nil
}

func (s *postgresStore) ListReadings(ctx context.Context, buoyID string, from, to *time.Time, limit, offset int) ([]reading, error) {
	ctx, done := s.observe(ctx, "list_readings")
	defer done()
	rows, err := s.pool.Query(ctx, `SELECT id, buoy_id, water_temp, wave_height, salinity, recorded_at FROM readings WHERE ($1::text='' OR buoy_id=NULLIF($1,'')::uuid) AND ($2::timestamptz IS NULL OR recorded_at >= $2) AND ($3::timestamptz IS NULL OR recorded_at <= $3) ORDER BY recorded_at DESC LIMIT $4 OFFSET $5`, buoyID, from, to, limit, offset)
	if err != nil {
		return nil, fmt.Errorf("query readings: %w", err)
	}
	defer rows.Close()
	items, err := pgx.CollectRows(rows, pgx.RowToStructByName[reading])
	if err != nil {
		return nil, fmt.Errorf("collect readings: %w", err)
	}
	return items, nil
}

func (s *postgresStore) CreateReading(ctx context.Context, r reading) (reading, error) {
	ctx, done := s.observe(ctx, "create_reading")
	defer done()
	err := s.pool.QueryRow(ctx, `INSERT INTO readings (id,buoy_id,water_temp,wave_height,salinity,recorded_at) VALUES ($1,$2,$3,$4,$5,COALESCE($6,now())) RETURNING recorded_at`, r.ID, r.BuoyID, r.WaterTemp, r.WaveHeight, r.Salinity, nullableTime(r.RecordedAt)).Scan(&r.RecordedAt)
	if err != nil {
		return reading{}, fmt.Errorf("insert reading: %w", err)
	}
	return r, nil
}

func nullableTime(value time.Time) any {
	if value.IsZero() {
		return nil
	}
	return value
}

func (s *postgresStore) AggregateReadings(ctx context.Context, window time.Duration) ([]aggregate, error) {
	ctx, done := s.observe(ctx, "aggregate_readings")
	defer done()
	seconds := int64(window.Seconds())
	rows, err := s.pool.Query(ctx, `SELECT to_timestamp(floor(extract(epoch FROM recorded_at)/$1)*$1) AS bucket, avg(water_temp) AS average_water_temp, avg(wave_height) AS average_wave_height, avg(salinity) AS average_salinity, count(*) AS count FROM readings GROUP BY bucket ORDER BY bucket DESC`, seconds)
	if err != nil {
		return nil, fmt.Errorf("aggregate readings: %w", err)
	}
	defer rows.Close()
	items, err := pgx.CollectRows(rows, pgx.RowToStructByName[aggregate])
	if err != nil {
		return nil, fmt.Errorf("collect aggregates: %w", err)
	}
	return items, nil
}
