package db

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"reflect"

	"github.com/coreos/go-oidc/oidc"
	"github.com/go-gorp/gorp"

	"github.com/coreos/dex/client"
	"github.com/coreos/dex/pkg/log"
	"github.com/coreos/dex/repo"
)

const (
	clientTableName      = "client_identity"
	trustedPeerTableName = "trusted_peers"

	// postgres error codes
	pgErrorCodeUniqueViolation = "23505" // unique_violation
)

var (
	localHostRedirectURL = mustParseURL("http://localhost:0")
)

func init() {
	register(table{
		name:    clientTableName,
		model:   clientModel{},
		autoinc: false,
		pkey:    []string{"id"},
	})

	register(table{
		name:    trustedPeerTableName,
		model:   trustedPeerModel{},
		autoinc: false,
		pkey:    []string{"client_id", "trusted_client_id"},
	})
}

func newClientModel(cli client.Client) (*clientModel, error) {
	hashed, err := client.HashSecret(cli.Credentials)
	if err != nil {
		return nil, err
	}

	if cli.Public {
		// Metadata.Valid(), and therefore json.Unmarshal(metadata) complains
		// when there's no RedirectURIs, so we set them to a fixed value here,
		// and remove it when translating back to a client.Client
		cli.Metadata.RedirectURIs = []url.URL{
			localHostRedirectURL,
		}
	}

	bmeta, err := json.Marshal(&cli.Metadata)
	if err != nil {
		return nil, err
	}

	cim := clientModel{
		ID:       cli.Credentials.ID,
		Secret:   hashed,
		Metadata: string(bmeta),
		DexAdmin: cli.Admin,
		Public:   cli.Public,
	}

	return &cim, nil
}

type clientModel struct {
	ID       string `db:"id"`
	Secret   []byte `db:"secret"`
	Metadata string `db:"metadata"`
	DexAdmin bool   `db:"dex_admin"`
	Public   bool   `db:"public"`
}

type trustedPeerModel struct {
	ClientID        string `db:"client_id"`
	TrustedClientID string `db:"trusted_client_id"`
}

func (m *clientModel) Client() (*client.Client, error) {
	ci := client.Client{
		Credentials: oidc.ClientCredentials{
			ID: m.ID,
		},
		Admin:  m.DexAdmin,
		Public: m.Public,
	}

	if err := json.Unmarshal([]byte(m.Metadata), &ci.Metadata); err != nil {
		return nil, err
	}

	if ci.Public {
		ci.Metadata.RedirectURIs = []url.URL{}
	}

	return &ci, nil
}

func NewClientRepo(dbm *gorp.DbMap) client.ClientRepo {
	return newClientRepo(dbm)
}

func newClientRepo(dbm *gorp.DbMap) *clientRepo {
	return &clientRepo{
		db: &db{dbm},
	}
}

type clientRepo struct {
	*db
}

func (r *clientRepo) Get(tx repo.Transaction, clientID string) (client.Client, error) {
	m, err := r.executor(tx).Get(clientModel{}, clientID)
	if err == sql.ErrNoRows || m == nil {
		return client.Client{}, client.ErrorNotFound
	}
	if err != nil {
		return client.Client{}, err
	}

	cim, ok := m.(*clientModel)
	if !ok {
		log.Errorf("expected clientModel but found %v", reflect.TypeOf(m))
		return client.Client{}, errors.New("unrecognized model")
	}

	ci, err := cim.Client()
	if err != nil {
		return client.Client{}, err
	}

	return *ci, nil
}

func (r *clientRepo) GetSecret(tx repo.Transaction, clientID string) ([]byte, error) {
	m, err := r.getModel(tx, clientID)
	if err != nil || m == nil {
		return nil, err
	}
	return m.Secret, nil
}

func (r *clientRepo) Update(tx repo.Transaction, cli client.Client) error {
	if cli.Credentials.ID == "" {
		return client.ErrorNotFound
	}
	// make sure this client exists already
	_, err := r.get(tx, cli.Credentials.ID)
	if err != nil {
		return err
	}
	err = r.update(tx, cli)
	if err != nil {
		return err
	}
	return nil
}

var alreadyExistsCheckers []func(err error) bool

func registerAlreadyExistsChecker(f func(err error) bool) {
	alreadyExistsCheckers = append(alreadyExistsCheckers, f)
}

// isAlreadyExistsErr detects database error codes for failing a unique constraint.
//
// Because database drivers are optionally compiled, use registerAlreadyExistsChecker to
// register driver specific implementations.
func isAlreadyExistsErr(err error) bool {
	for _, checker := range alreadyExistsCheckers {
		if checker(err) {
			return true
		}
	}
	return false
}

func (r *clientRepo) New(tx repo.Transaction, cli client.Client) (*oidc.ClientCredentials, error) {
	cim, err := newClientModel(cli)
	if err != nil {
		return nil, err
	}

	if err := r.executor(tx).Insert(cim); err != nil {
		if isAlreadyExistsErr(err) {
			return nil, client.ErrorDuplicateClientID
		}
		return nil, err
	}

	cc := oidc.ClientCredentials{
		ID:     cli.Credentials.ID,
		Secret: cli.Credentials.Secret,
	}

	return &cc, nil
}

func (r *clientRepo) All(tx repo.Transaction) ([]client.Client, error) {
	qt := r.quote(clientTableName)
	q := fmt.Sprintf("SELECT * FROM %s", qt)
	objs, err := r.executor(tx).Select(&clientModel{}, q)
	if err != nil {
		return nil, err
	}

	cs := make([]client.Client, len(objs))
	for i, obj := range objs {
		m, ok := obj.(*clientModel)
		if !ok {
			return nil, errors.New("unable to cast client identity to clientModel")
		}

		ci, err := m.Client()
		if err != nil {
			return nil, err
		}
		cs[i] = *ci
	}
	return cs, nil
}

func NewClientRepoFromClients(dbm *gorp.DbMap, cs []client.LoadableClient) (client.ClientRepo, error) {
	repo := NewClientRepo(dbm).(*clientRepo)
	for _, c := range cs {
		cm, err := newClientModel(c.Client)
		if err != nil {
			return nil, err
		}
		err = repo.executor(nil).Insert(cm)
		if err != nil {
			return nil, err
		}

		err = repo.SetTrustedPeers(nil, c.Client.Credentials.ID, c.TrustedPeers)
		if err != nil {
			return nil, err
		}

	}
	return repo, nil
}

func (r *clientRepo) get(tx repo.Transaction, clientID string) (client.Client, error) {
	cm, err := r.getModel(tx, clientID)
	if err != nil {
		return client.Client{}, err
	}

	cli, err := cm.Client()
	if err != nil {
		return client.Client{}, err
	}

	return *cli, nil
}

func (r *clientRepo) getModel(tx repo.Transaction, clientID string) (*clientModel, error) {
	ex := r.executor(tx)

	m, err := ex.Get(clientModel{}, clientID)
	if err != nil {
		return nil, err
	}

	if m == nil {
		return nil, client.ErrorNotFound
	}

	cm, ok := m.(*clientModel)
	if !ok {
		log.Errorf("expected clientModel but found %v", reflect.TypeOf(m))
		return nil, errors.New("unrecognized model")
	}
	return cm, nil
}

func (r *clientRepo) update(tx repo.Transaction, cli client.Client) error {
	ex := r.executor(tx)
	cm, err := newClientModel(cli)
	if err != nil {
		return err
	}
	_, err = ex.Update(cm)
	return err
}

func (r *clientRepo) GetTrustedPeers(tx repo.Transaction, clientID string) ([]string, error) {
	ex := r.executor(tx)
	if clientID == "" {
		return nil, client.ErrorInvalidClientID
	}

	qt := r.quote(trustedPeerTableName)
	var ids []string
	_, err := ex.Select(&ids, fmt.Sprintf("SELECT trusted_client_id from %s where client_id = $1", qt), clientID)

	if err != nil {
		if err != sql.ErrNoRows {
			return nil, err
		}
		return nil, nil
	}

	return ids, nil
}

func (r *clientRepo) SetTrustedPeers(tx repo.Transaction, clientID string, clientIDs []string) error {
	ex := r.executor(tx)
	qt := r.quote(trustedPeerTableName)

	// First delete all existing rows
	_, err := ex.Exec(fmt.Sprintf("DELETE from %s where client_id = $1", qt), clientID)
	if err != nil {
		return err
	}

	// Ensure that the client exists.
	_, err = r.get(tx, clientID)
	if err != nil {
		return err
	}

	// Set the clients
	rows := []interface{}{}
	for _, curID := range clientIDs {
		rows = append(rows, &trustedPeerModel{
			ClientID:        clientID,
			TrustedClientID: curID,
		})
	}
	err = ex.Insert(rows...)
	if err != nil {
		return err
	}

	return nil
}

func mustParseURL(s string) url.URL {
	u, err := url.Parse(s)
	if err != nil {
		panic(err)
	}
	return *u
}
