package barfdb

import (
	"context"
	"crypto/rand"
	"database/sql"
	"database/sql/driver"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
)

type Dataset struct {
	CurrentPhase                int    `json:"currentPhase"`
	CurrentPhaseDialog          int    `json:"currentPhaseDialog"`
	AudioEnabled                bool   `json:"audioEnabled"`
	Name                        string `json:"name"`
	DiscordUsername             string `json:"discordUsername"`
	ReceiveEmail                bool   `json:"receiveEmail"`
	Email                       string `json:"email"`
	DateSat31August             string `json:"dateSat31August"`
	DateSat7September           string `json:"dateSat7September"`
	ActivityEscapeRoom          string `json:"activityEscapeRoom"`
	ActivityPub                 string `json:"activityPub"`
	ActivityMeanGirlsTheMusical string `json:"activityMeanGirlsTheMusical"`
	ActivityKaraoke             string `json:"activityKaraoke"`
	AccommodationRequired       bool   `json:"accommodationRequired"`
	TravelCosts                 bool   `json:"travelCosts"`
	Misc                        string `json:"misc"`
}

func (ds *Dataset) Scan(value any) error {
	return json.Unmarshal([]byte(value.(string)), ds)
}

func (ds *Dataset) Value() (driver.Value, error) {
	b, err := json.Marshal(ds)
	return string(b), err
}

func (ds *Dataset) HumanString() (string, error) {
	bs, err := json.MarshalIndent(ds, "", "  ")
	if err != nil {
		return "", err
	}
	return string(bs), nil
}

type DB struct {
	db *sql.DB
}

func New(sdb *sql.DB) *DB {
	return &DB{sdb}
}

func (d *DB) Close() error {
	return d.db.Close()
}

var ErrNoSuchKey = errors.New("barfdb: no such key")

const (
	keyBytes  = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	keyLength = 12
)

func (d *DB) Initialize(ctx context.Context) error {
	_, err := d.db.ExecContext(ctx, "CREATE TABLE responses (key STRING PRIMARY KEY NOT NULL, responses JSONB NOT NULL)")
	return err
}

type Entry struct {
	FullKey string
	Dataset Dataset
}

func (e Entry) BaseKey() string {
	if strings.Contains(e.FullKey, "/") {
		return e.FullKey[:strings.Index(e.FullKey, "/")]
	}
	return e.FullKey
}

func (e Entry) Completed() bool {
	mustHaveValue := []string{
		e.Dataset.DiscordUsername,
		e.Dataset.Name,
		e.Dataset.ActivityEscapeRoom,
		e.Dataset.ActivityKaraoke,
		e.Dataset.ActivityMeanGirlsTheMusical,
		e.Dataset.ActivityPub,
		e.Dataset.DateSat31August,
		e.Dataset.DateSat7September,
	}
	for _, v := range mustHaveValue {
		if v == "" {
			return false
		}
	}
	return true
}

func (d *DB) List(ctx context.Context) ([]Entry, error) {
	rows, err := d.db.QueryContext(ctx, "SELECT key, responses FROM responses ORDER BY key")
	if err != nil {
		return nil, fmt.Errorf("querying database: %w", err)
	}
	var entries []Entry
	for rows.Next() {
		var (
			fullKey string
			dataset Dataset
		)
		if err := rows.Scan(&fullKey, &dataset); err != nil {
			return nil, fmt.Errorf("scanning row from database: %w", err)
		}

		entry := Entry{
			FullKey: fullKey,
			Dataset: dataset,
		}
		entries = append(entries, entry)
	}
	return entries, nil
}

func (d *DB) GetOrCreateKey(ctx context.Context, baseKey string, initialDataset *Dataset) (string, error) {
	var existingKey string
	err := d.db.QueryRowContext(ctx, `SELECT key FROM responses WHERE key LIKE ? || '/%'`, baseKey).Scan(&existingKey)
	if err == nil {
		return existingKey, nil
	} else if errors.Is(err, sql.ErrNoRows) {
		// This is fine, we'll create it.
	} else {
		return "", fmt.Errorf("querying database for existing keys for base %q: %w", baseKey, err)
	}

	randomKeyBytes := make([]byte, keyLength)
	if _, err := rand.Read(randomKeyBytes); err != nil {
		return "", fmt.Errorf("generating random key: %w", err)
	}

	randomKeyStringBytes := make([]byte, keyLength)
	for n, b := range randomKeyBytes {
		randomKeyStringBytes[n] = keyBytes[int(b)%len(keyBytes)]
	}
	randomKey := string(randomKeyStringBytes)
	fullKey := baseKey + "/" + randomKey

	if _, err := d.db.ExecContext(ctx, `INSERT INTO responses (key, responses) VALUES (?, ?)`, fullKey, initialDataset); err != nil {
		return "", fmt.Errorf("inserting into responses table: %w", err)
	}
	return fullKey, nil
}

func (d *DB) GetResponseForShortKey(ctx context.Context, shortKey string) (*Dataset, error) {
	var response Dataset
	err := d.db.QueryRowContext(ctx, `SELECT responses FROM responses WHERE key = ? OR key LIKE ? || '/%'`, shortKey, shortKey).Scan(&response)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrNoSuchKey
	} else if err != nil {
		return nil, fmt.Errorf("fetching %v from database: %w", shortKey, err)
	}
	return &response, nil
}

func (d *DB) GetResponseForKey(ctx context.Context, key string) (*Dataset, error) {
	var response Dataset
	err := d.db.QueryRowContext(ctx, `SELECT responses FROM responses WHERE key = ?`, key).Scan(&response)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrNoSuchKey
	} else if err != nil {
		return nil, fmt.Errorf("fetching %v from database: %w", key, err)
	}
	return &response, nil
}

func (d *DB) SaveResponseForKey(ctx context.Context, key string, dataset *Dataset) error {
	res, err := d.db.ExecContext(ctx, `UPDATE responses SET responses = ? WHERE key = ?`, dataset, key)
	if err != nil {
		return fmt.Errorf("updating database for %v: %w", key, err)
	}
	rowCount, err := res.RowsAffected()
	if err != nil {
		return fmt.Errorf("determining affected row count for %v: %w", key, err)
	} else if rowCount == 0 {
		return ErrNoSuchKey
	}
	return nil
}

func NewDB(db *sql.DB) *DB {
	return &DB{db: db}
}