package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/url"
	"regexp"
	"strings"
	"time"

	"github.com/asaskevich/govalidator"
	"github.com/gliderlabs/ssh"
	"github.com/jinzhu/gorm"
)

type Config struct {
	SSHKeys    []*SSHKey    `json:"keys"`
	Hosts      []*Host      `json:"hosts"`
	UserKeys   []*UserKey   `json:"user_keys"`
	Users      []*User      `json:"users"`
	UserGroups []*UserGroup `json:"user_groups"`
	HostGroups []*HostGroup `json:"host_groups"`
	ACLs       []*ACL       `json:"acls"`
	Settings   []*Setting   `json:"settings"`
	Events     []*Event     `json:"events"`
	Sessions   []*Session   `json:"sessions"`
	// FIXME: add latest migration
	Date time.Time `json:"date"`
}

type Setting struct {
	gorm.Model
	Name  string `valid:"required"`
	Value string `valid:"required"`
}

// SSHKey defines a ssh client key (used by sshportal to connect to remote hosts)
type SSHKey struct {
	// FIXME: use uuid for ID
	gorm.Model
	Name        string  `valid:"required,length(1|32),unix_user"`
	Type        string  `valid:"required"`
	Length      uint    `valid:"required"`
	Fingerprint string  `valid:"optional"`
	PrivKey     string  `sql:"size:10000" valid:"required"`
	PubKey      string  `sql:"size:10000" valid:"optional"`
	Hosts       []*Host `gorm:"ForeignKey:SSHKeyID"`
	Comment     string  `valid:"optional"`
}

type Host struct {
	// FIXME: use uuid for ID
	gorm.Model
	Name     string       `gorm:"size:32" valid:"required,length(1|32),unix_user"`
	Addr     string       `valid:"required"`
	User     string       `valid:"optional"`
	Password string       `valid:"optional"`
	SSHKey   *SSHKey      `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
	SSHKeyID uint         `gorm:"index"`
	HostKey  []byte       `sql:"size:10000" valid:"optional"`
	Groups   []*HostGroup `gorm:"many2many:host_host_groups;"`
	Comment  string       `valid:"optional"`
}

// UserKey defines a user public key used by sshportal to identify the user
type UserKey struct {
	gorm.Model
	Key           []byte `sql:"size:10000" valid:"required,length(1|10000)"`
	AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
	UserID        uint   ``
	User          *User  `gorm:"ForeignKey:UserID"`
	Comment       string `valid:"optional"`
}

type UserRole struct {
	gorm.Model
	Name  string  `valid:"required,length(1|32),unix_user"`
	Users []*User `gorm:"many2many:user_user_roles"`
}

type User struct {
	// FIXME: use uuid for ID
	gorm.Model
	Roles       []*UserRole  `gorm:"many2many:user_user_roles"`
	Email       string       `valid:"required,email"`
	Name        string       `valid:"required,length(1|32),unix_user"`
	Keys        []*UserKey   `gorm:"ForeignKey:UserID"`
	Groups      []*UserGroup `gorm:"many2many:user_user_groups;"`
	Comment     string       `valid:"optional"`
	InviteToken string       `valid:"optional,length(10|60)"`
}

type UserGroup struct {
	gorm.Model
	Name    string  `valid:"required,length(1|32),unix_user"`
	Users   []*User `gorm:"many2many:user_user_groups;"`
	ACLs    []*ACL  `gorm:"many2many:user_group_acls;"`
	Comment string  `valid:"optional"`
}

type HostGroup struct {
	gorm.Model
	Name    string  `valid:"required,length(1|32),unix_user"`
	Hosts   []*Host `gorm:"many2many:host_host_groups;"`
	ACLs    []*ACL  `gorm:"many2many:host_group_acls;"`
	Comment string  `valid:"optional"`
}

type ACL struct {
	gorm.Model
	HostGroups  []*HostGroup `gorm:"many2many:host_group_acls;"`
	UserGroups  []*UserGroup `gorm:"many2many:user_group_acls;"`
	HostPattern string       `valid:"optional"`
	Action      string       `valid:"required"`
	Weight      uint         ``
	Comment     string       `valid:"optional"`
}

type Session struct {
	gorm.Model
	StoppedAt *time.Time `sql:"index" valid:"optional"`
	Status    string     `valid:"required"`
	User      *User      `gorm:"ForeignKey:UserID"`
	Host      *Host      `gorm:"ForeignKey:HostID"`
	UserID    uint       `valid:"optional"`
	HostID    uint       `valid:"optional"`
	ErrMsg    string     `valid:"optional"`
	Comment   string     `valid:"optional"`
}

type Event struct {
	gorm.Model
	Author   *User                  `gorm:"ForeignKey:AuthorID"`
	AuthorID uint                   `valid:"optional"`
	Domain   string                 `valid:"required"`
	Action   string                 `valid:"required"`
	Entity   string                 `valid:"optional"`
	Args     []byte                 `sql:"size:10000" valid:"optional,length(1|10000)" json:"-"`
	ArgsMap  map[string]interface{} `gorm:"-" json:"Args"`
}

type SessionStatus string

const (
	SessionStatusUnknown SessionStatus = "unknown"
	SessionStatusActive                = "active"
	SessionStatusClosed                = "closed"
)

type ACLAction string

const (
	ACLActionUnknown ACLAction = "unknown"
	ACLActionAllow             = "allow"
	ACLActionDeny              = "deny"
)

func init() {
	unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")

	govalidator.CustomTypeTagMap.Set("unix_user", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
		name, ok := i.(string)
		if !ok {
			return false
		}
		return unixUserRegexp.MatchString(name)
	}))
}

func RemoteHostFromSession(s ssh.Session, db *gorm.DB) (*Host, error) {
	var host Host
	db.Preload("SSHKey").Where("name = ?", s.User()).Find(&host)
	if host.Name == "" {
		// FIXME: add available hosts
		return nil, fmt.Errorf("No such target: %q", s.User())
	}
	return &host, nil
}

func (host *Host) URL() string {
	return fmt.Sprintf("%s@%s", host.User, host.Addr)
}

func NewHostFromURL(rawurl string) (*Host, error) {
	if !strings.Contains(rawurl, "://") {
		rawurl = "ssh://" + rawurl
	}
	u, err := url.Parse(rawurl)
	if err != nil {
		return nil, err
	}
	host := Host{Addr: u.Host}
	if !strings.Contains(host.Addr, ":") {
		host.Addr += ":22" // add port if not present
	}
	host.User = "root" // default username
	if u.User != nil {
		password, _ := u.User.Password()
		host.Password = password
		host.User = u.User.Username()
	}
	return &host, nil
}

func (host *Host) Hostname() string {
	return strings.Split(host.Addr, ":")[0]
}

// Host helpers

func HostsPreload(db *gorm.DB) *gorm.DB {
	return db.Preload("Groups").Preload("SSHKey")
}
func HostsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
	return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
}

// SSHKey helpers

func SSHKeysPreload(db *gorm.DB) *gorm.DB {
	return db.Preload("Hosts")
}
func SSHKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
	return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
}

// HostGroup helpers

func HostGroupsPreload(db *gorm.DB) *gorm.DB {
	return db.Preload("ACLs").Preload("Hosts")
}
func HostGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
	return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
}

// UserGroup helpers

func UserGroupsPreload(db *gorm.DB) *gorm.DB {
	return db.Preload("ACLs").Preload("Users")
}
func UserGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
	return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
}

// User helpers

func UsersPreload(db *gorm.DB) *gorm.DB {
	return db.Preload("Groups").Preload("Keys").Preload("Roles")
}
func UsersByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
	return db.Where("id IN (?)", identifiers).Or("email IN (?)", identifiers).Or("name IN (?)", identifiers)
}
func UserHasRole(user User, name string) bool {
	for _, role := range user.Roles {
		if role.Name == name {
			return true
		}
	}
	return false
}
func UserCheckRoles(user User, names []string) error {
	ok := false
	for _, name := range names {
		if UserHasRole(user, name) {
			ok = true
			break
		}
	}
	if ok {
		return nil
	}
	return fmt.Errorf("you don't have permission to access this feature (requires any of these roles: '%s')", strings.Join(names, "', '"))
}

// ACL helpers

func ACLsPreload(db *gorm.DB) *gorm.DB {
	return db.Preload("UserGroups").Preload("HostGroups")
}
func ACLsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
	return db.Where("id IN (?)", identifiers)
}

// UserKey helpers

func UserKeysPreload(db *gorm.DB) *gorm.DB {
	return db.Preload("User")
}
func UserKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
	return db.Where("id IN (?)", identifiers)
}

// UserRole helpers

func UserRolesPreload(db *gorm.DB) *gorm.DB {
	return db.Preload("Users")
}
func UserRolesByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
	return db.Where("id IN (?)", identifiers).Or("name IN (?)", identifiers)
}

// Session helpers

func SessionsPreload(db *gorm.DB) *gorm.DB {
	return db.Preload("User").Preload("Host")
}
func SessionsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
	return db.Where("id IN (?)", identifiers)
}

// Events helpers

func EventsPreload(db *gorm.DB) *gorm.DB {
	return db.Preload("Author")
}
func EventsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
	return db.Where("id IN (?)", identifiers)
}

func NewEvent(domain, action string) *Event {
	return &Event{
		Domain:  domain,
		Action:  action,
		ArgsMap: map[string]interface{}{},
	}
}

func (e *Event) String() string {
	return fmt.Sprintf("%s %s %s %s", e.Domain, e.Action, e.Entity, string(e.Args))
}

func (e *Event) Log(db *gorm.DB) {
	if len(e.ArgsMap) > 0 {
		var err error
		if e.Args, err = json.Marshal(e.ArgsMap); err != nil {
			log.Printf("error: %v", err)
		}
	}
	log.Printf("info: %s", e)
	if err := db.Create(e).Error; err != nil {
		log.Printf("warning: %v", err)
	}
}

func (e *Event) SetAuthor(user *User) *Event {
	e.Author = user
	e.AuthorID = user.ID
	return e
}

func (e *Event) SetArg(name string, value interface{}) *Event {
	e.ArgsMap[name] = value
	return e
}
