package mailinglist

import (
	"crypto/sha512"
	"database/sql"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"path"
	"strings"
	"time"

	_ "github.com/mattn/go-sqlite3"
	"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
	migrate "github.com/rubenv/sql-migrate"
)

var (
	// ErrNotFound is used to indicate an email could not be found in the
	// database.
	ErrNotFound = errors.New("no record found")
)

// EmailIterator will iterate through a sequence of emails, returning the next
// email in the sequence on each call, or returning io.EOF.
type EmailIterator func() (Email, error)

// Email describes all information related to an email which has yet
// to be verified.
type Email struct {
	Email     string
	SubToken  string
	CreatedAt time.Time

	UnsubToken string
	VerifiedAt time.Time
}

// Store is used for storing MailingList related information.
type Store interface {

	// Set is used to set the information related to an email.
	Set(Email) error

	// Get will return the record for the given email, or ErrNotFound.
	Get(email string) (Email, error)

	// GetBySubToken will return the record for the given SubToken, or
	// ErrNotFound.
	GetBySubToken(subToken string) (Email, error)

	// GetByUnsubToken will return the record for the given UnsubToken, or
	// ErrNotFound.
	GetByUnsubToken(unsubToken string) (Email, error)

	// Delete will delete the record for the given email.
	Delete(email string) error

	// GetAll returns all emails for which there is a record.
	GetAll() EmailIterator

	Close() error
}

var migrations = []*migrate.Migration{
	&migrate.Migration{
		Id: "1",
		Up: []string{
			`CREATE TABLE emails (
				id          TEXT PRIMARY KEY,
				email       TEXT NOT NULL,
				sub_token   TEXT NOT NULL,
				created_at  INTEGER NOT NULL,

				unsub_token TEXT,
				verified_at INTEGER
			)`,
		},
		Down: []string{"DROP TABLE emails"},
	},
}

type store struct {
	db *sql.DB
}

// NewStore initializes a new Store using a sqlite3 database in the given
// DataDir.
func NewStore(dataDir cfg.DataDir) (Store, error) {

	path := path.Join(dataDir.Path, "mailinglist.sqlite3")

	db, err := sql.Open("sqlite3", path)
	if err != nil {
		return nil, fmt.Errorf("opening sqlite file at %q: %w", path, err)
	}

	migrations := &migrate.MemoryMigrationSource{Migrations: migrations}

	if _, err := migrate.Exec(db, "sqlite3", migrations, migrate.Up); err != nil {
		return nil, fmt.Errorf("running migrations: %w", err)
	}

	return &store{
		db: db,
	}, nil
}

func (s *store) emailID(email string) string {
	email = strings.ToLower(email)
	h := sha512.New()
	h.Write([]byte(email))
	return base64.URLEncoding.EncodeToString(h.Sum(nil))
}

func (s *store) Set(email Email) error {
	_, err := s.db.Exec(
		`INSERT INTO emails (
			id, email, sub_token, created_at, unsub_token, verified_at
		)
		VALUES
		(?, ?, ?, ?, ?, ?)
		ON CONFLICT (id) DO UPDATE SET
			email=excluded.email,
			sub_token=excluded.sub_token,
			unsub_token=excluded.unsub_token,
			verified_at=excluded.verified_at
		`,
		s.emailID(email.Email),
		email.Email,
		email.SubToken,
		email.CreatedAt.Unix(),
		email.UnsubToken,
		sql.NullInt64{
			Int64: email.VerifiedAt.Unix(),
			Valid: !email.VerifiedAt.IsZero(),
		},
	)

	return err
}

var scanCols = []string{
	"email", "sub_token", "created_at", "unsub_token", "verified_at",
}

type row interface {
	Scan(...interface{}) error
}

func (s *store) scanRow(row row) (Email, error) {
	var email Email
	var createdAt int64
	var verifiedAt sql.NullInt64

	err := row.Scan(
		&email.Email,
		&email.SubToken,
		&createdAt,
		&email.UnsubToken,
		&verifiedAt,
	)
	if err != nil {
		return Email{}, err
	}

	email.CreatedAt = time.Unix(createdAt, 0)
	if verifiedAt.Valid {
		email.VerifiedAt = time.Unix(verifiedAt.Int64, 0)
	}

	return email, nil
}

func (s *store) scanSingleRow(row *sql.Row) (Email, error) {
	email, err := s.scanRow(row)
	if errors.Is(err, sql.ErrNoRows) {
		return Email{}, ErrNotFound
	}

	return email, err
}

func (s *store) Get(email string) (Email, error) {
	row := s.db.QueryRow(
		`SELECT `+strings.Join(scanCols, ",")+`
		FROM emails
		WHERE id=?`,
		s.emailID(email),
	)

	return s.scanSingleRow(row)
}

func (s *store) GetBySubToken(subToken string) (Email, error) {
	row := s.db.QueryRow(
		`SELECT `+strings.Join(scanCols, ",")+`
		FROM emails
		WHERE sub_token=?`,
		subToken,
	)

	return s.scanSingleRow(row)
}

func (s *store) GetByUnsubToken(unsubToken string) (Email, error) {
	row := s.db.QueryRow(
		`SELECT `+strings.Join(scanCols, ",")+`
		FROM emails
		WHERE unsub_token=?`,
		unsubToken,
	)

	return s.scanSingleRow(row)
}

func (s *store) Delete(email string) error {
	_, err := s.db.Exec(
		`DELETE FROM emails WHERE id=?`,
		s.emailID(email),
	)
	return err
}

func (s *store) GetAll() EmailIterator {
	rows, err := s.db.Query(
		`SELECT ` + strings.Join(scanCols, ",") + `
		FROM emails`,
	)

	return func() (Email, error) {
		if err != nil {
			return Email{}, err

		} else if !rows.Next() {
			return Email{}, io.EOF
		}
		return s.scanRow(rows)
	}
}

func (s *store) Close() error {
	return s.db.Close()
}
