package auth

import (
	"time"

	"code.secondbit.org/uuid.hg"
	"github.com/lib/pq"
	"github.com/secondbit/pan"
)

func (p Profile) GetSQLTableName() string {
	return "profiles"
}

func (l Login) GetSQLTableName() string {
	return "logins"
}

func (p *postgres) getProfileByIDSQL(id uuid.ID) *pan.Query {
	var profile Profile
	fields, _ := pan.GetFields(profile)
	query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(profile))
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(profile, "ID")+" = ?", id)
	return query.FlushExpressions(" ")
}

func (p *postgres) getProfileByID(id uuid.ID) (Profile, error) {
	query := p.getProfileByIDSQL(id)
	rows, err := p.db.Query(query.String(), query.Args...)
	if err != nil {
		return Profile{}, err
	}
	var profile Profile
	var found bool
	for rows.Next() {
		err := pan.Unmarshal(rows, &profile)
		if err != nil {
			return Profile{}, err
		}
		found = true
	}
	if err := rows.Err(); err != nil {
		return Profile{}, err
	}
	if !found {
		return profile, ErrProfileNotFound
	}
	return profile, nil
}

func (p *postgres) getProfileByLoginSQL(value string) *pan.Query {
	var profile Profile
	var login Login
	fields, _ := pan.GetUnquotedAbsoluteFields(profile)
	query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(profile))
	query.Include("INNER JOIN " + pan.GetTableName(login))
	query.Include("ON " + pan.GetUnquotedAbsoluteColumn(profile, "ID") + " = " + pan.GetUnquotedAbsoluteColumn(login, "ProfileID"))
	query.IncludeWhere()
	query.Include(pan.GetUnquotedAbsoluteColumn(login, "Value")+" = ?", value)
	return query.FlushExpressions(" ")
}

func (p *postgres) getProfileByLogin(value string) (Profile, error) {
	query := p.getProfileByLoginSQL(value)
	rows, err := p.db.Query(query.String(), query.Args...)
	if err != nil {
		return Profile{}, err
	}
	var profile Profile
	var found bool
	for rows.Next() {
		err := pan.Unmarshal(rows, &profile)
		if err != nil {
			return Profile{}, err
		}
		found = true
	}
	if err := rows.Err(); err != nil {
		return Profile{}, err
	}
	if !found {
		return profile, ErrProfileNotFound
	}
	return profile, nil
}

func (p *postgres) saveProfileSQL(profile Profile) *pan.Query {
	fields, values := pan.GetFields(profile)
	query := pan.New(pan.POSTGRES, "INSERT INTO "+pan.GetTableName(profile))
	query.Include("(" + pan.QueryList(fields) + ")")
	query.Include("VALUES")
	query.Include("("+pan.VariableList(len(values))+")", values...)
	return query.FlushExpressions(" ")
}

func (p *postgres) saveProfile(profile Profile) error {
	query := p.saveProfileSQL(profile)
	_, err := p.db.Exec(query.String(), query.Args...)
	if e, ok := err.(*pq.Error); ok && e.Constraint == "profiles_pkey" {
		err = ErrProfileAlreadyExists
	}
	return err
}

func (p *postgres) updateProfileSQL(id uuid.ID, change ProfileChange) *pan.Query {
	var profile Profile
	query := pan.New(pan.POSTGRES, "UPDATE "+pan.GetTableName(profile)+" SET ")
	query.IncludeIfNotNil(pan.GetUnquotedColumn(profile, "Name")+" = ?", change.Name)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(profile, "Passphrase")+" = ?", change.Passphrase)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(profile, "Iterations")+" = ?", change.Iterations)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(profile, "Salt")+" = ?", change.Salt)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(profile, "PassphraseScheme")+" = ?", change.PassphraseScheme)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(profile, "Compromised")+" = ?", change.Compromised)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(profile, "LockedUntil")+" = ?", change.LockedUntil)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(profile, "PassphraseReset")+" = ?", change.PassphraseReset)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(profile, "PassphraseResetCreated")+" = ?", change.PassphraseResetCreated)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(profile, "LastSeen")+" = ?", change.LastSeen)
	query.FlushExpressions(", ")
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(profile, "ID")+" = ?", id)
	return query.FlushExpressions(" ")
}

func (p *postgres) updateProfile(id uuid.ID, change ProfileChange) error {
	if change.Empty() {
		return nil
	}
	query := p.updateProfileSQL(id, change)
	_, err := p.db.Exec(query.String(), query.Args...)
	return err
}

func (p *postgres) updateProfilesSQL(ids []uuid.ID, change BulkProfileChange) *pan.Query {
	if change.Empty() {
		return nil
	}
	var profile Profile
	query := pan.New(pan.POSTGRES, "UPDATE "+pan.GetTableName(profile)+" SET ")
	query.IncludeIfNotNil(pan.GetUnquotedColumn(profile, "Compromised")+" = ?", change.Compromised)
	query.FlushExpressions(", ")
	query.IncludeWhere()
	intids := make([]interface{}, len(ids))
	for pos, id := range ids {
		intids[pos] = id
	}
	query.Include(pan.GetUnquotedColumn(profile, "ID")+" IN ("+pan.VariableList(len(ids))+")", intids...)
	return query.FlushExpressions(" ")
}

func (p *postgres) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
	query := p.updateProfilesSQL(ids, change)
	_, err := p.db.Exec(query.String(), query.Args...)
	return err
}

func (p *postgres) deleteProfileSQL(id uuid.ID) *pan.Query {
	var profile Profile
	query := pan.New(pan.POSTGRES, "DELETE FROM "+pan.GetTableName(profile))
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(profile, "ID")+" = ?", id)
	return query.FlushExpressions(" ")
}

func (p *postgres) deleteProfile(id uuid.ID) error {
	query := p.deleteProfileSQL(id)
	res, err := p.db.Exec(query.String(), query.Args...)
	if err != nil {
		return err
	}
	rows, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if rows == 0 {
		return ErrProfileNotFound
	}
	return nil
}

func (p *postgres) addLoginSQL(login Login) *pan.Query {
	fields, values := pan.GetFields(login)
	query := pan.New(pan.POSTGRES, "INSERT INTO "+pan.GetTableName(login))
	query.Include("(" + pan.QueryList(fields) + ")")
	query.Include("VALUES")
	query.Include("("+pan.VariableList(len(values))+")", values...)
	return query.FlushExpressions(" ")
}

func (p *postgres) addLogin(login Login) error {
	query := p.addLoginSQL(login)
	_, err := p.db.Exec(query.String(), query.Args...)
	if e, ok := err.(*pq.Error); ok && e.Constraint == "logins_pkey" {
		return ErrLoginAlreadyExists
	}
	return err
}

func (p *postgres) removeLoginsByProfileSQL(profile uuid.ID) *pan.Query {
	var login Login
	query := pan.New(pan.POSTGRES, "DELETE FROM "+pan.GetTableName(login))
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(login, "ProfileID")+" = ?", profile)
	return query.FlushExpressions(" ")
}

func (p *postgres) removeLoginsByProfile(profile uuid.ID) error {
	query := p.removeLoginsByProfileSQL(profile)
	res, err := p.db.Exec(query.String(), query.Args...)
	if err != nil {
		return err
	}
	rows, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if rows == 0 {
		return ErrProfileNotFound
	}
	return nil
}

func (p *postgres) removeLoginSQL(value string, profile uuid.ID) *pan.Query {
	var login Login
	query := pan.New(pan.POSTGRES, "DELETE FROM "+pan.GetTableName(login))
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(login, "Value")+" = ? AND "+pan.GetUnquotedColumn(login, "ProfileID")+" = ?", value, profile)
	return query.FlushExpressions(" ")
}

func (p *postgres) removeLogin(value string, profile uuid.ID) error {
	query := p.removeLoginSQL(value, profile)
	res, err := p.db.Exec(query.String(), query.Args...)
	if err != nil {
		return err
	}
	rows, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if rows == 0 {
		return ErrLoginNotFound
	}
	return nil
}

func (p *postgres) recordLoginUseSQL(value string, when time.Time) *pan.Query {
	var login Login
	query := pan.New(pan.POSTGRES, "UPDATE "+pan.GetTableName(login)+" SET ")
	query.Include(pan.GetUnquotedColumn(login, "LastUsed")+" = ?", when)
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(login, "Value")+" = ?", value)
	return query.FlushExpressions(" ")
}

func (p *postgres) recordLoginUse(value string, when time.Time) error {
	query := p.recordLoginUseSQL(value, when)
	_, err := p.db.Exec(query.String(), query.Args...)
	return err
}

func (p *postgres) verifyLoginSQL(value, verification string) *pan.Query {
	var login Login
	query := pan.New(pan.POSTGRES, "UPDATE "+pan.GetTableName(login)+" SET ")
	query.Include(pan.GetUnquotedColumn(login, "Verified")+" = ?", true)
	query.IncludeWhere()
	query.FlushExpressions(" ")
	query.Include(pan.GetUnquotedColumn(login, "Value")+" = ?", value)
	query.Include(pan.GetUnquotedColumn(login, "Verification")+" = ?", verification)
	return query.FlushExpressions(" AND ")
}

func (p *postgres) verifyLogin(value, verification string) error {
	query := p.verifyLoginSQL(value, verification)
	res, err := p.db.Exec(query.String(), query.Args...)
	if err != nil {
		return err
	}
	rows, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if rows == 0 {
		return ErrLoginVerificationInvalid
	}
	return nil
}

func (p *postgres) getLoginSQL(value string) *pan.Query {
	var login Login
	fields, _ := pan.GetFields(login)
	query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(login))
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(login, "Value")+" = ?", value)
	return query.FlushExpressions(" ")
}

func (p *postgres) getLogin(value string) (Login, error) {
	query := p.getLoginSQL(value)
	rows, err := p.db.Query(query.String(), query.Args...)
	if err != nil {
		return Login{}, err
	}
	var login Login
	var found bool
	for rows.Next() {
		err := pan.Unmarshal(rows, &login)
		if err != nil {
			return Login{}, err
		}
		found = true
	}
	if err := rows.Err(); err != nil {
		return Login{}, err
	}
	if !found {
		return login, ErrLoginNotFound
	}
	return login, nil
}

func (p *postgres) listLoginsSQL(profile uuid.ID, num, offset int) *pan.Query {
	var login Login
	fields, _ := pan.GetFields(login)
	query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(login))
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(login, "ProfileID")+" = ?", profile)
	query.IncludeLimit(int64(num))
	query.IncludeOffset(int64(offset))
	return query.FlushExpressions(" ")
}

func (p *postgres) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
	query := p.listLoginsSQL(profile, num, offset)
	rows, err := p.db.Query(query.String(), query.Args...)
	if err != nil {
		return []Login{}, err
	}
	var logins []Login
	for rows.Next() {
		var login Login
		err = pan.Unmarshal(rows, &login)
		if err != nil {
			return logins, err
		}
		logins = append(logins, login)
	}
	if err := rows.Err(); err != nil {
		return logins, err
	}
	return logins, nil
}
