// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0

package mocks

import (
	"context"
	"sort"
	"strconv"
	"strings"
	"sync"

	"github.com/mainflux/mainflux/bootstrap"
	"github.com/mainflux/mainflux/pkg/errors"
)

const emptyState = -1

var _ bootstrap.ConfigRepository = (*configRepositoryMock)(nil)

type configRepositoryMock struct {
	mu       sync.Mutex
	counter  uint64
	configs  map[string]bootstrap.Config
	channels map[string]bootstrap.Channel
}

// NewConfigsRepository creates in-memory config repository.
func NewConfigsRepository() bootstrap.ConfigRepository {
	return &configRepositoryMock{
		configs:  make(map[string]bootstrap.Config),
		channels: make(map[string]bootstrap.Channel),
	}
}

func (crm *configRepositoryMock) Save(_ context.Context, config bootstrap.Config, connections []string) (string, error) {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	for _, v := range crm.configs {
		if v.ThingID == config.ThingID || v.ExternalID == config.ExternalID {
			return "", errors.ErrConflict
		}
	}

	crm.counter++
	config.ThingID = strconv.FormatUint(crm.counter, 10)
	crm.configs[config.ThingID] = config

	for _, ch := range config.Channels {
		crm.channels[ch.ID] = ch
	}

	config.Channels = []bootstrap.Channel{}

	for _, ch := range connections {
		config.Channels = append(config.Channels, crm.channels[ch])
	}

	crm.configs[config.ThingID] = config

	return config.ThingID, nil
}

func (crm *configRepositoryMock) RetrieveByID(_ context.Context, token, id string) (bootstrap.Config, error) {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	c, ok := crm.configs[id]
	if !ok {
		return bootstrap.Config{}, errors.ErrNotFound
	}
	if c.Owner != token {
		return bootstrap.Config{}, errors.ErrAuthentication
	}

	return c, nil
}

func (crm *configRepositoryMock) RetrieveAll(_ context.Context, token string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	configs := make([]bootstrap.Config, 0)

	first := uint64(offset) + 1
	last := first + uint64(limit)
	var state bootstrap.State = emptyState
	var name string
	if s, ok := filter.FullMatch["state"]; ok {
		val, _ := strconv.Atoi(s)
		state = bootstrap.State(val)
	}

	if s, ok := filter.PartialMatch["name"]; ok {
		name = strings.ToLower(s)
	}

	var total uint64
	for _, v := range crm.configs {
		id, _ := strconv.ParseUint(v.ThingID, 10, 64)
		if (state == emptyState || v.State == state) &&
			(name == "" || strings.Contains(strings.ToLower(v.Name), name)) &&
			v.Owner == token {
			if id >= first && id < last {
				configs = append(configs, v)
			}
			total++
		}
	}

	sort.SliceStable(configs, func(i, j int) bool {
		return configs[i].ThingID < configs[j].ThingID
	})

	return bootstrap.ConfigsPage{
		Total:   total,
		Offset:  offset,
		Limit:   limit,
		Configs: configs,
	}
}

func (crm *configRepositoryMock) RetrieveByExternalID(_ context.Context, externalID string) (bootstrap.Config, error) {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	for _, cfg := range crm.configs {
		if cfg.ExternalID == externalID {
			return cfg, nil
		}
	}

	return bootstrap.Config{}, errors.ErrNotFound
}

func (crm *configRepositoryMock) Update(_ context.Context, config bootstrap.Config) error {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	cfg, ok := crm.configs[config.ThingID]
	if !ok || cfg.Owner != config.Owner {
		return errors.ErrNotFound
	}

	cfg.Name = config.Name
	cfg.Content = config.Content
	crm.configs[config.ThingID] = cfg

	return nil
}

func (crm *configRepositoryMock) UpdateCert(_ context.Context, owner, thingID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
	crm.mu.Lock()
	defer crm.mu.Unlock()
	var forUpdate bootstrap.Config
	for _, v := range crm.configs {
		if v.ThingID == thingID && v.Owner == owner {
			forUpdate = v
			break
		}
	}
	if _, ok := crm.configs[forUpdate.ThingID]; !ok {
		return bootstrap.Config{}, errors.ErrNotFound
	}
	forUpdate.ClientCert = clientCert
	forUpdate.ClientKey = clientKey
	forUpdate.CACert = caCert
	crm.configs[forUpdate.ThingID] = forUpdate

	return forUpdate, nil
}

func (crm *configRepositoryMock) UpdateConnections(_ context.Context, token, id string, channels []bootstrap.Channel, connections []string) error {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	config, ok := crm.configs[id]
	if !ok {
		return errors.ErrNotFound
	}

	for _, ch := range channels {
		crm.channels[ch.ID] = ch
	}

	config.Channels = []bootstrap.Channel{}
	for _, conn := range connections {
		ch, ok := crm.channels[conn]
		if !ok {
			return errors.ErrNotFound
		}
		config.Channels = append(config.Channels, ch)
	}
	crm.configs[id] = config

	return nil
}

func (crm *configRepositoryMock) Remove(_ context.Context, token, id string) error {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	for k, v := range crm.configs {
		if v.Owner == token && k == id {
			delete(crm.configs, k)
			break
		}
	}

	return nil
}

func (crm *configRepositoryMock) ChangeState(_ context.Context, token, id string, state bootstrap.State) error {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	config, ok := crm.configs[id]
	if !ok {
		return errors.ErrNotFound
	}
	if config.Owner != token {
		return errors.ErrAuthentication
	}

	config.State = state
	crm.configs[id] = config
	return nil
}

func (crm *configRepositoryMock) ListExisting(_ context.Context, token string, connections []string) ([]bootstrap.Channel, error) {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	var ret []bootstrap.Channel

	for k, v := range crm.channels {
		for _, conn := range connections {
			if conn == k {
				ret = append(ret, v)
				break
			}
		}
	}

	return ret, nil
}

func (crm *configRepositoryMock) RemoveThing(_ context.Context, id string) error {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	delete(crm.configs, id)
	return nil
}

func (crm *configRepositoryMock) UpdateChannel(_ context.Context, ch bootstrap.Channel) error {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	channel, ok := crm.channels[ch.ID]
	if !ok {
		return nil
	}

	channel.Name = ch.Name
	channel.Metadata = ch.Metadata
	crm.channels[ch.ID] = channel
	return nil
}

func (crm *configRepositoryMock) RemoveChannel(_ context.Context, id string) error {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	delete(crm.channels, id)
	return nil
}

func (crm *configRepositoryMock) DisconnectThing(_ context.Context, channelID, thingID string) error {
	crm.mu.Lock()
	defer crm.mu.Unlock()

	idx := -1
	if config, ok := crm.configs[thingID]; ok {
		for i, ch := range config.Channels {
			if ch.ID == channelID {
				idx = i
				break
			}
		}

		if idx != -1 {
			config.Channels = append(config.Channels[0:idx], config.Channels[idx:]...)
		}
		crm.configs[thingID] = config
	}

	delete(crm.channels, channelID)

	return nil
}
