// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package user

import (
	"context"
	"fmt"

	"github.com/drone/drone/core"
	"github.com/drone/drone/store/shared/db"
	"github.com/drone/drone/store/shared/encrypt"
)

// New returns a new UserStore.
func New(db *db.DB, enc encrypt.Encrypter) core.UserStore {
	return &userStore{db, enc}
}

type userStore struct {
	db  *db.DB
	enc encrypt.Encrypter
}

// Find returns a user from the datastore.
func (s *userStore) Find(ctx context.Context, id int64) (*core.User, error) {
	out := &core.User{ID: id}
	err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
		params := map[string]interface{}{"user_id": id}
		query, args, err := binder.BindNamed(queryKey, params)
		if err != nil {
			return err
		}
		row := queryer.QueryRow(query, args...)
		return scanRow(s.enc, row, out)
	})
	return out, err
}

// FindLogin returns a user from the datastore by username.
func (s *userStore) FindLogin(ctx context.Context, login string) (*core.User, error) {
	out := &core.User{Login: login}
	err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
		params := map[string]interface{}{"user_login": login}
		query, args, err := binder.BindNamed(queryLogin, params)
		if err != nil {
			return err
		}
		row := queryer.QueryRow(query, args...)
		return scanRow(s.enc, row, out)
	})
	return out, err
}

// FindToken returns a user from the datastore by token.
func (s *userStore) FindToken(ctx context.Context, token string) (*core.User, error) {
	out := &core.User{Hash: token}
	err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
		params := map[string]interface{}{"user_hash": token}
		query, args, err := binder.BindNamed(queryToken, params)
		if err != nil {
			return err
		}
		row := queryer.QueryRow(query, args...)
		return scanRow(s.enc, row, out)
	})
	return out, err
}

// List returns a list of users from the datastore.
func (s *userStore) List(ctx context.Context) ([]*core.User, error) {
	var out []*core.User
	err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
		rows, err := queryer.Query(queryAll)
		if err != nil {
			return err
		}
		out, err = scanRows(s.enc, rows)
		return err
	})
	return out, err
}

// ListRange returns a list of users from the datastore.
func (s *userStore) ListRange(ctx context.Context, params core.UserParams) ([]*core.User, error) {
	var out []*core.User
	err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
		// this query breaks a rule and uses sprintf to inject parameters
		// into the query. Normally this should be avoided, however, in this
		// case the parameters are set by the internal system and can
		// be considered safe.
		query := queryRange
		switch {
		case params.Sort:
			query = fmt.Sprintf(query, "user_login", params.Size, params.Page)
		default:
			query = fmt.Sprintf(query, "user_id", params.Size, params.Page)
		}
		rows, err := queryer.Query(query)
		if err != nil {
			return err
		}
		out, err = scanRows(s.enc, rows)
		return err
	})
	return out, err
}

// Create persists a new user to the datastore.
func (s *userStore) Create(ctx context.Context, user *core.User) error {
	if s.db.Driver() == db.Postgres {
		return s.createPostgres(ctx, user)
	}
	return s.create(ctx, user)
}

func (s *userStore) create(ctx context.Context, user *core.User) error {
	return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
		params, err := toParams(s.enc, user)
		if err != nil {
			return err
		}
		stmt, args, err := binder.BindNamed(stmtInsert, params)
		if err != nil {
			return err
		}
		res, err := execer.Exec(stmt, args...)
		if err != nil {
			return err
		}
		user.ID, err = res.LastInsertId()
		return err
	})
}

func (s *userStore) createPostgres(ctx context.Context, user *core.User) error {
	return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
		params, err := toParams(s.enc, user)
		if err != nil {
			return err
		}
		stmt, args, err := binder.BindNamed(stmtInsertPg, params)
		if err != nil {
			return err
		}
		return execer.QueryRow(stmt, args...).Scan(&user.ID)
	})
}

// Update persists an updated user to the datastore.
func (s *userStore) Update(ctx context.Context, user *core.User) error {
	return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
		params, err := toParams(s.enc, user)
		if err != nil {
			return err
		}
		stmt, args, err := binder.BindNamed(stmtUpdate, params)
		if err != nil {
			return err
		}
		_, err = execer.Exec(stmt, args...)
		return err
	})
}

// Delete deletes a user from the datastore.
func (s *userStore) Delete(ctx context.Context, user *core.User) error {
	return s.db.Lock(func(execer db.Execer, binder db.Binder) error {
		params := map[string]interface{}{"user_id": user.ID}
		stmt, args, err := binder.BindNamed(stmtDelete, params)
		if err != nil {
			return err
		}
		_, err = execer.Exec(stmt, args...)
		return err
	})
}

// Count returns a count of active users.
func (s *userStore) Count(ctx context.Context) (int64, error) {
	var out int64
	err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
		return queryer.QueryRow(queryCount).Scan(&out)
	})
	return out, err
}

// Count returns a count of active human users.
func (s *userStore) CountHuman(ctx context.Context) (int64, error) {
	var out int64
	err := s.db.View(func(queryer db.Queryer, binder db.Binder) error {
		params := map[string]interface{}{"user_machine": false}
		stmt, args, err := binder.BindNamed(queryCountHuman, params)
		if err != nil {
			return err
		}
		return queryer.QueryRow(stmt, args...).Scan(&out)
	})
	return out, err
}

const queryCount = `
SELECT COUNT(*)
FROM users
`

const queryCountHuman = `
SELECT COUNT(*)
FROM users
WHERE user_machine = :user_machine
`

const queryBase = `
SELECT
 user_id
,user_login
,user_email
,user_admin
,user_machine
,user_active
,user_avatar
,user_syncing
,user_synced
,user_created
,user_updated
,user_last_login
,user_oauth_token
,user_oauth_refresh
,user_oauth_expiry
,user_hash
`

const queryKey = queryBase + `
FROM users
WHERE user_id = :user_id
`

const queryLogin = queryBase + `
FROM users
WHERE user_login = :user_login
`

const queryToken = queryBase + `
FROM users
WHERE user_hash = :user_hash
`

const queryAll = queryBase + `
FROM users
ORDER BY user_login
`

const queryRange = queryBase + `
FROM users
ORDER BY %s
LIMIT %d
OFFSET %d
`

const stmtUpdate = `
UPDATE users
SET
 user_email         = :user_email
,user_admin         = :user_admin
,user_active        = :user_active
,user_avatar        = :user_avatar
,user_syncing       = :user_syncing
,user_synced        = :user_synced
,user_created       = :user_created
,user_updated       = :user_updated
,user_last_login    = :user_last_login
,user_oauth_token   = :user_oauth_token
,user_oauth_refresh = :user_oauth_refresh
,user_oauth_expiry  = :user_oauth_expiry
,user_hash          = :user_hash
WHERE user_id = :user_id
`

const stmtDelete = `
DELETE FROM users WHERE user_id = :user_id
`

const stmtInsert = `
INSERT INTO users (
 user_login
,user_email
,user_admin
,user_machine
,user_active
,user_avatar
,user_syncing
,user_synced
,user_created
,user_updated
,user_last_login
,user_oauth_token
,user_oauth_refresh
,user_oauth_expiry
,user_hash
) VALUES (
 :user_login
,:user_email
,:user_admin
,:user_machine
,:user_active
,:user_avatar
,:user_syncing
,:user_synced
,:user_created
,:user_updated
,:user_last_login
,:user_oauth_token
,:user_oauth_refresh
,:user_oauth_expiry
,:user_hash
)
`

const stmtInsertPg = stmtInsert + `
RETURNING user_id
`
