package database

import (
	"bytes"
	"database/sql"
	"log"

	"git.xeserv.us/xena/route/internal/database/dmigrations"
	"github.com/Xe/uuid"
	"github.com/brandur/simplebox"
	"github.com/gchaincl/dotsql"
	_ "github.com/lib/pq"
	"golang.org/x/net/context"
)

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

type PostgresStorage struct {
	ds *dotsql.DotSql
	db *sql.DB
	sb *simplebox.SimpleBox

	//cs *postgresCertificateStorage
	rs *postgresRouteStorage
	//ts *postgresTokenStorage
}

type postgresCertificateStorage struct {
	*PostgresStorage
}

type postgresRouteStorage struct {
	*PostgresStorage
}

type postgresTokenStorage struct {
	*PostgresStorage
}

// NewPostgresStorage creates a new Storage instance backed by postgres at the
// given URL.
func NewPostgresStorage(url string, key *[32]byte) (Storage, error) {
	db, err := sql.Open("postgres", url)
	if err != nil {
		return nil, err
	}

	data, err := dmigrations.Asset("postgres.sql")
	if err != nil {
		return nil, err
	}

	buf := bytes.NewBuffer(data)

	ds, err := dotsql.Load(buf)
	if err != nil {
		return nil, err
	}

	for k := range ds.QueryMap() {
		log.Printf("preparing %s", k)
		stmt, err := ds.Prepare(db, k)
		if err != nil {
			db.Close()
			return nil, err
		}
		defer stmt.Close()
	}

	p := &PostgresStorage{
		db: db,
		ds: ds,
		sb: simplebox.NewFromSecretKey(key),
	}

	//p.cs = &postgresCertificateStorage{p}
	p.rs = &postgresRouteStorage{p}
	//p.ts = &postgresTokenStorage{p}

	return p, nil
}

// Certs gets the certificate storage interface.
func (p *PostgresStorage) Certs() Certs { return nil }

// Routes gets the route storage interface.
func (p *PostgresStorage) Routes() Routes { return p.rs }

// Tokens gets the token storage interface.
func (p *PostgresStorage) Tokens() Tokens { return nil }

// Close cleans up resources for this Storage.
func (p *PostgresStorage) Close() error { return p.db.Close() }

// interface compliance
var (
	_ Storage = &PostgresStorage{}
	//_ Certs   = &postgresCertificateStorage{}
	_ Routes = &postgresRouteStorage{}
	//_ Tokens  = &postgresTokenStorage{}
)

func (p *postgresRouteStorage) getRouteInner(ctx context.Context, arg string, kind string) (Route, error) {
	r, err := p.ds.QueryRow(p.db, kind, arg)
	if err != nil {
		return Route{}, err
	}

	rt := Route{}

	err = (&rt).Scan(r)
	if err != nil {
		return Route{}, err
	}

	return rt, nil
}

func (p *postgresRouteStorage) Get(ctx context.Context, id string) (Route, error) {
	return p.getRouteInner(ctx, id, "find-one-route-by-id")
}

func (p *postgresRouteStorage) GetHost(ctx context.Context, host string) (Route, error) {
	return p.getRouteInner(ctx, host, "find-one-route-by-host")
}

func (p *postgresRouteStorage) GetAll(ctx context.Context, user string) ([]Route, error) {
	var result []Route

	rows, err := p.ds.Query(p.db, "find-all-routes-for-user", user)
	if err != nil {
		return nil, err
	}

	defer rows.Close()
	for rows.Next() {
		rt := &Route{}

		if err := rt.Scan(rows); err != nil {
			return nil, err
		}

		result = append(result, *rt)
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return result, nil
}

func (p *postgresRouteStorage) Put(ctx context.Context, r Route) (Route, error) {
	if r.ID == "" {
		r.ID = uuid.New()
	}

	_, err := p.ds.Exec(p.db, "insert-route", r.ID, r.Hostname)
	if err != nil {
		return Route{}, err
	}

	return p.Get(ctx, r.ID)
}

func (p *postgresRouteStorage) Delete(ctx context.Context, r Route) (Route, error) {
	rt, err := p.Get(ctx, r.ID)
	if err != nil {
		return Route{}, err
	}

	_, err = p.ds.Exec(p.db, "delete-one-route", rt.ID, rt.Hostname)
	if err != nil {
		return Route{}, err
	}

	return rt, nil
}
