package inmem

import (
	"errors"
	"fmt"
	"reflect"
	"sync"
	"time"

	"github.com/kolide/kolide/server/config"
	"github.com/kolide/kolide/server/datastore/internal/appstate"
	"github.com/kolide/kolide/server/kolide"
	"github.com/patrickmn/sortutil"
)

type Datastore struct {
	Driver  string
	mtx     sync.RWMutex
	nextIDs map[interface{}]uint

	users                           map[uint]*kolide.User
	sessions                        map[uint]*kolide.Session
	passwordResets                  map[uint]*kolide.PasswordResetRequest
	invites                         map[uint]*kolide.Invite
	labels                          map[uint]*kolide.Label
	labelQueryExecutions            map[uint]*kolide.LabelQueryExecution
	queries                         map[uint]*kolide.Query
	packs                           map[uint]*kolide.Pack
	hosts                           map[uint]*kolide.Host
	scheduledQueries                map[uint]*kolide.ScheduledQuery
	packTargets                     map[uint]*kolide.PackTarget
	distributedQueryExecutions      map[uint]kolide.DistributedQueryExecution
	distributedQueryCampaigns       map[uint]kolide.DistributedQueryCampaign
	distributedQueryCampaignTargets map[uint]kolide.DistributedQueryCampaignTarget
	options                         map[uint]*kolide.Option
	decorators                      map[uint]*kolide.Decorator
	filePaths                       map[uint]*kolide.FIMSection
	yaraFilePaths                   kolide.YARAFilePaths
	yaraSignatureGroups             map[uint]*kolide.YARASignatureGroup
	appConfig                       *kolide.AppConfig
	config                          *config.KolideConfig
}

func New(config config.KolideConfig) (*Datastore, error) {
	ds := &Datastore{
		Driver: "inmem",
		config: &config,
	}

	if err := ds.MigrateTables(); err != nil {
		return nil, err
	}

	return ds, nil
}

func (d *Datastore) Name() string {
	return "inmem"
}

func sortResults(slice interface{}, opt kolide.ListOptions, fields map[string]string) error {
	field, ok := fields[opt.OrderKey]
	if !ok {
		return errors.New("cannot sort on unknown key: " + opt.OrderKey)
	}

	if opt.OrderDirection == kolide.OrderDescending {
		sortutil.DescByField(slice, field)
	} else {
		sortutil.AscByField(slice, field)
	}

	return nil
}

func (d *Datastore) MigrateTables() error {
	d.mtx.Lock()
	defer d.mtx.Unlock()

	d.nextIDs = make(map[interface{}]uint)
	d.users = make(map[uint]*kolide.User)
	d.sessions = make(map[uint]*kolide.Session)
	d.passwordResets = make(map[uint]*kolide.PasswordResetRequest)
	d.invites = make(map[uint]*kolide.Invite)
	d.labels = make(map[uint]*kolide.Label)
	d.labelQueryExecutions = make(map[uint]*kolide.LabelQueryExecution)
	d.queries = make(map[uint]*kolide.Query)
	d.packs = make(map[uint]*kolide.Pack)
	d.hosts = make(map[uint]*kolide.Host)
	d.scheduledQueries = make(map[uint]*kolide.ScheduledQuery)
	d.packTargets = make(map[uint]*kolide.PackTarget)
	d.distributedQueryExecutions = make(map[uint]kolide.DistributedQueryExecution)
	d.distributedQueryCampaigns = make(map[uint]kolide.DistributedQueryCampaign)
	d.distributedQueryCampaignTargets = make(map[uint]kolide.DistributedQueryCampaignTarget)
	d.options = make(map[uint]*kolide.Option)
	d.decorators = make(map[uint]*kolide.Decorator)
	d.filePaths = make(map[uint]*kolide.FIMSection)
	d.yaraFilePaths = make(kolide.YARAFilePaths)
	d.yaraSignatureGroups = make(map[uint]*kolide.YARASignatureGroup)

	return nil
}

func (d *Datastore) MigrateData() error {
	for _, initData := range appstate.Options() {
		opt := kolide.Option{
			Name:     initData.Name,
			Value:    kolide.OptionValue{Val: initData.Value},
			Type:     initData.Type,
			ReadOnly: initData.ReadOnly,
		}
		opt.ID = d.nextID(opt)
		d.options[opt.ID] = &opt
	}

	d.appConfig = &kolide.AppConfig{
		ID:                 1,
		SMTPEnableTLS:      true,
		SMTPPort:           587,
		SMTPEnableStartTLS: true,
		SMTPVerifySSLCerts: true,
	}

	if err := d.createBuiltinLabels(); err != nil {
		return err
	}

	return nil
}

func (d *Datastore) Drop() error {
	return d.MigrateTables()
}

func (d *Datastore) Initialize() error {
	if err := d.createDevUsers(); err != nil {
		return err
	}

	if err := d.createDevHosts(); err != nil {
		return err
	}

	if err := d.createDevQueries(); err != nil {
		return err
	}

	if err := d.createDevLabels(); err != nil {
		return err
	}

	if err := d.createDevOrgInfo(); err != nil {
		return err
	}

	if err := d.createDevPacksAndQueries(); err != nil {
		return err
	}

	return nil
}

// getLimitOffsetSliceBounds returns the bounds that should be used for
// re-slicing the results to comply with the requested ListOptions. Lack of
// generics forces us to do this rather than reslicing in this method.
func (d *Datastore) getLimitOffsetSliceBounds(opt kolide.ListOptions, length int) (low uint, high uint) {
	if opt.PerPage == 0 {
		// PerPage value of 0 indicates unlimited
		return 0, uint(length)
	}

	offset := opt.Page * opt.PerPage
	max := offset + opt.PerPage
	if offset > uint(length) {
		offset = uint(length)
	}
	if max > uint(length) {
		max = uint(length)
	}
	return offset, max
}

// nextID returns the next ID value that should be used for a struct of the
// given type
func (d *Datastore) nextID(val interface{}) uint {
	valType := reflect.TypeOf(reflect.Indirect(reflect.ValueOf(val)).Interface())
	d.nextIDs[valType]++
	return d.nextIDs[valType]
}

func (d *Datastore) createDevPacksAndQueries() error {
	query1 := &kolide.Query{
		Name:  "Osquery Info",
		Query: "select * from osquery_info",
	}
	query1, err := d.NewQuery(query1)
	if err != nil {
		return err
	}

	query2 := &kolide.Query{
		Name:  "Launchd",
		Query: "select * from launchd",
	}
	query2, err = d.NewQuery(query2)
	if err != nil {
		return err
	}

	query3 := &kolide.Query{
		Name:  "registry",
		Query: "select * from osquery_registry",
	}
	query3, err = d.NewQuery(query3)
	if err != nil {
		return err
	}

	pack1 := &kolide.Pack{
		Name: "Osquery Internal Info",
	}
	pack1, err = d.NewPack(pack1)
	if err != nil {
		return err
	}

	pack2 := &kolide.Pack{
		Name: "macOS Attacks",
	}
	pack2, err = d.NewPack(pack2)
	if err != nil {
		return err
	}

	_, err = d.NewScheduledQuery(&kolide.ScheduledQuery{
		QueryID:  query1.ID,
		PackID:   pack1.ID,
		Interval: 60,
	})
	if err != nil {
		return err
	}

	t := true
	_, err = d.NewScheduledQuery(&kolide.ScheduledQuery{
		QueryID:  query3.ID,
		PackID:   pack1.ID,
		Interval: 60,
		Snapshot: &t,
	})
	if err != nil {
		return err
	}

	_, err = d.NewScheduledQuery(&kolide.ScheduledQuery{
		QueryID:  query2.ID,
		PackID:   pack2.ID,
		Interval: 60,
	})
	if err != nil {
		return err
	}

	return nil
}

func (d *Datastore) createBuiltinLabels() error {
	for _, label := range appstate.Labels() {
		label := label
		_, err := d.NewLabel(&label)
		if err != nil {
			return err
		}
	}

	return nil
}

// Bootstrap a few users when using the in-memory database.
// Each user's default password will just be their username.
func (d *Datastore) createDevUsers() error {
	users := []kolide.User{
		{
			UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
				CreateTimestamp: kolide.CreateTimestamp{
					CreatedAt: time.Date(2016, time.October, 27, 10, 0, 0, 0, time.UTC),
				},
				UpdateTimestamp: kolide.UpdateTimestamp{
					UpdatedAt: time.Date(2016, time.October, 27, 10, 0, 0, 0, time.UTC),
				},
			},

			Name:     "Admin User",
			Username: "admin",
			Email:    "admin@kolide.co",
			Position: "Director of Security",
			Admin:    true,
			Enabled:  true,
		},
		{
			UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
				CreateTimestamp: kolide.CreateTimestamp{
					CreatedAt: time.Now().Add(-3 * time.Hour),
				},
				UpdateTimestamp: kolide.UpdateTimestamp{
					UpdatedAt: time.Now().Add(-1 * time.Hour),
				},
			},

			Name:     "Normal User",
			Username: "user",
			Email:    "user@kolide.co",
			Position: "Security Engineer",
			Admin:    false,
			Enabled:  true,
		},
	}
	for _, user := range users {
		user := user
		err := user.SetPassword(user.Username, d.config.Auth.SaltKeySize, d.config.Auth.BcryptCost)
		if err != nil {
			return nil
		}
		_, err = d.NewUser(&user)
		if err != nil {
			return err
		}
	}

	return nil
}

func (d *Datastore) createDevQueries() error {
	queries := []kolide.Query{
		{
			UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
				CreateTimestamp: kolide.CreateTimestamp{
					CreatedAt: time.Date(2016, time.October, 17, 7, 6, 0, 0, time.UTC),
				},
				UpdateTimestamp: kolide.UpdateTimestamp{
					UpdatedAt: time.Date(2016, time.October, 17, 7, 6, 0, 0, time.UTC),
				},
			},

			Name:  "dev_query_1",
			Query: "select * from processes",
		},
		{
			UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
				CreateTimestamp: kolide.CreateTimestamp{
					CreatedAt: time.Date(2016, time.October, 27, 4, 3, 10, 0, time.UTC),
				},
				UpdateTimestamp: kolide.UpdateTimestamp{
					UpdatedAt: time.Date(2016, time.October, 27, 4, 3, 10, 0, time.UTC),
				},
			},
			Name:  "dev_query_2",
			Query: "select * from time",
		},
		{
			UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
				CreateTimestamp: kolide.CreateTimestamp{
					CreatedAt: time.Now().Add(-24 * time.Hour),
				},
				UpdateTimestamp: kolide.UpdateTimestamp{
					UpdatedAt: time.Now().Add(-17 * time.Hour),
				},
			},

			Name:  "dev_query_3",
			Query: "select * from cpuid",
		},
		{
			UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
				CreateTimestamp: kolide.CreateTimestamp{
					CreatedAt: time.Now().Add(-1 * time.Hour),
				},
				UpdateTimestamp: kolide.UpdateTimestamp{
					UpdatedAt: time.Now().Add(-30 * time.Hour),
				},
			},

			Name:  "dev_query_4",
			Query: "select 1 from processes where name like '%Apache%'",
		},
		{
			UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
				CreateTimestamp: kolide.CreateTimestamp{
					CreatedAt: time.Now(),
				},
				UpdateTimestamp: kolide.UpdateTimestamp{
					UpdatedAt: time.Now(),
				},
			},
			Name:  "dev_query_5",
			Query: "select 1 from osquery_info where build_platform='darwin'",
		},
	}

	for _, query := range queries {
		query := query
		_, err := d.NewQuery(&query)
		if err != nil {
			return err
		}
	}

	return nil
}

// Bootstrap a few hosts when using the in-memory database.
func (d *Datastore) createDevHosts() error {
	hosts := []kolide.Host{
		{
			UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
				CreateTimestamp: kolide.CreateTimestamp{
					CreatedAt: time.Date(2016, time.October, 27, 10, 0, 0, 0, time.UTC),
				},
				UpdateTimestamp: kolide.UpdateTimestamp{
					UpdatedAt: time.Now().Add(-20 * time.Minute),
				},
			},
			NodeKey:          "totally-legit",
			HostName:         "jmeller-mbp.local",
			UUID:             "1234-5678-9101",
			Platform:         "darwin",
			OsqueryVersion:   "2.0.0",
			OSVersion:        "Mac OS X 10.11.6",
			Uptime:           60 * time.Minute,
			PhysicalMemory:   4145483776,
			DetailUpdateTime: time.Now().Add(-20 * time.Minute),
		},
		{
			UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
				CreateTimestamp: kolide.CreateTimestamp{
					CreatedAt: time.Date(2016, time.October, 27, 4, 3, 10, 0, time.UTC),
				},
				UpdateTimestamp: kolide.UpdateTimestamp{
					UpdatedAt: time.Date(2016, time.October, 27, 4, 3, 10, 0, time.UTC),
				},
			},

			NodeKey:          "definitely-legit",
			HostName:         "marpaia.local",
			UUID:             "1234-5678-9102",
			Platform:         "windows",
			OsqueryVersion:   "2.0.0",
			OSVersion:        "Windows 10.0.0",
			Uptime:           60 * time.Minute,
			PhysicalMemory:   17179869184,
			DetailUpdateTime: time.Now().Add(-10 * time.Second),
		},
	}

	for _, host := range hosts {
		host := host
		_, err := d.NewHost(&host)
		if err != nil {
			return err
		}
	}

	return nil
}

func (d *Datastore) createDevOrgInfo() error {
	devOrgInfo := &kolide.AppConfig{
		KolideServerURL:        "http://localhost:8080",
		OrgName:                "Kolide",
		OrgLogoURL:             fmt.Sprintf("https://%s/assets/images/kolide-logo.svg", d.config.Server.Address),
		SMTPPort:               587,
		SMTPAuthenticationType: kolide.AuthTypeUserNamePassword,
		SMTPEnableTLS:          true,
		SMTPVerifySSLCerts:     true,
		SMTPEnableStartTLS:     true,
	}
	_, err := d.NewAppConfig(devOrgInfo)
	if err != nil {
		return err
	}
	return nil
}

func (d *Datastore) createDevLabels() error {
	labels := []kolide.Label{
		{
			UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
				CreateTimestamp: kolide.CreateTimestamp{
					CreatedAt: time.Date(2016, time.October, 27, 8, 31, 16, 0, time.UTC),
				},
				UpdateTimestamp: kolide.UpdateTimestamp{
					UpdatedAt: time.Date(2016, time.October, 27, 8, 31, 16, 0, time.UTC),
				},
			},
			Name:  "dev_label_apache",
			Query: "select * from processes where name like '%Apache%'",
		},
		{
			UpdateCreateTimestamps: kolide.UpdateCreateTimestamps{
				CreateTimestamp: kolide.CreateTimestamp{
					CreatedAt: time.Now().Add(-1 * time.Hour),
				},
				UpdateTimestamp: kolide.UpdateTimestamp{
					UpdatedAt: time.Now(),
				},
			},

			Name:  "dev_label_darwin",
			Query: "select * from osquery_info where build_platform='darwin'",
		},
	}

	for _, label := range labels {
		label := label
		_, err := d.NewLabel(&label)
		if err != nil {
			return err
		}
	}

	return nil
}
