// Package store owns all database access. Photo tables use the photos_
// prefix and are migrated by this service alone; the handful of reads
// against Drizzle-owned tables (users, events, tickets, payments) are
// confined to access.go.
package store

import (
	"database/sql"
	"fmt"
	"strings"
	"time"

	_ "github.com/jackc/pgx/v5/stdlib"
	_ "modernc.org/sqlite"

	"git.azzamo.net/Michilis/Spanglish/photo-api/internal/config"
)

const (
	Postgres = "postgres"
	SQLite   = "sqlite"

	// timeLayout is a fixed-width UTC format so SQLite text timestamps
	// sort correctly; the backend stores ISO strings in SQLite the same way.
	timeLayout = "2006-01-02T15:04:05.000Z"
)

type DB struct {
	*sql.DB
	Type string
}

func Open(cfg config.Config) (*DB, error) {
	switch cfg.DBType {
	case Postgres:
		sqlDB, err := sql.Open("pgx", cfg.DatabaseURL)
		if err != nil {
			return nil, fmt.Errorf("open postgres: %w", err)
		}
		sqlDB.SetMaxOpenConns(10)
		return &DB{DB: sqlDB, Type: Postgres}, nil
	case SQLite:
		dsn := "file:" + strings.TrimPrefix(cfg.DatabaseURL, "file:") +
			"?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)"
		sqlDB, err := sql.Open("sqlite", dsn)
		if err != nil {
			return nil, fmt.Errorf("open sqlite: %w", err)
		}
		// One writer at a time keeps the shared file friendly to the backend.
		sqlDB.SetMaxOpenConns(1)
		return &DB{DB: sqlDB, Type: SQLite}, nil
	default:
		return nil, fmt.Errorf("unsupported DB_TYPE %q", cfg.DBType)
	}
}

// IsUniqueViolation reports whether err is a duplicate-key error. The two
// drivers wrap it in unrelated types (pgconn.PgError vs sqlite.Error), and
// neither is worth importing here just for one check, so this matches on the
// message: pgx renders "(SQLSTATE 23505)", modernc/sqlite "UNIQUE constraint
// failed: ...".
func IsUniqueViolation(err error) bool {
	if err == nil {
		return false
	}
	msg := err.Error()
	return strings.Contains(msg, "SQLSTATE 23505") || strings.Contains(msg, "UNIQUE constraint failed")
}

// Rebind converts ?-style placeholders to $n for Postgres. Queries in this
// package never contain literal question marks in strings.
func (db *DB) Rebind(query string) string {
	if db.Type != Postgres {
		return query
	}
	var b strings.Builder
	n := 0
	for _, r := range query {
		if r == '?' {
			n++
			fmt.Fprintf(&b, "$%d", n)
		} else {
			b.WriteRune(r)
		}
	}
	return b.String()
}

// TimeArg converts a time for binding: time.Time for Postgres (timestamptz
// columns), fixed-layout UTC text for SQLite (text columns).
func (db *DB) TimeArg(t time.Time) any {
	if db.Type == Postgres {
		return t.UTC()
	}
	return t.UTC().Format(timeLayout)
}

// asString normalizes scanned values across drivers.
func asString(v any) string {
	switch x := v.(type) {
	case nil:
		return ""
	case string:
		return x
	case []byte:
		return string(x)
	case time.Time:
		return x.UTC().Format(timeLayout)
	default:
		return fmt.Sprintf("%v", x)
	}
}

// asTime parses a scanned timestamp from either driver; zero time on failure.
func asTime(v any) time.Time {
	switch x := v.(type) {
	case time.Time:
		return x.UTC()
	case string:
		return parseTime(x)
	case []byte:
		return parseTime(string(x))
	default:
		return time.Time{}
	}
}

func parseTime(s string) time.Time {
	for _, layout := range []string{timeLayout, time.RFC3339Nano, time.RFC3339, "2006-01-02 15:04:05.999999999-07:00", "2006-01-02 15:04:05"} {
		if t, err := time.Parse(layout, s); err == nil {
			return t.UTC()
		}
	}
	return time.Time{}
}
