// Copyright 2021 Northern.tech AS
//
//    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 app

import (
	"context"
	"encoding/json"
	"time"

	"github.com/google/uuid"
	"github.com/pkg/errors"

	"github.com/mendersoftware/mender-server/pkg/api/client"
	"github.com/mendersoftware/mender-server/pkg/identity"
	"github.com/mendersoftware/mender-server/pkg/requestid"

	"github.com/mendersoftware/mender-server/services/deviceconfig/model"
	"github.com/mendersoftware/mender-server/services/deviceconfig/store"
)

// App errors
var (
	ErrDeviceNotFound     = errors.New("device not found")
	ErrDeviceNotConnected = errors.New("device not connected")
)

// App interface describes app objects
//
//nolint:lll
//go:generate ../../../utils/mockgen.sh
type App interface {
	HealthCheck(ctx context.Context) error

	ProvisionTenant(ctx context.Context, tenant model.NewTenant) error
	DeleteTenant(ctx context.Context, tenant_id string) error

	ProvisionDevice(ctx context.Context, dev model.NewDevice) error
	DecommissionDevice(ctx context.Context, devID string) error

	SetConfiguration(ctx context.Context, devID string, configuration model.Attributes) error
	UpdateConfiguration(ctx context.Context, devID string, attrs model.Attributes) error
	SetReportedConfiguration(ctx context.Context, devID string, configuration model.Attributes) error
	GetDevice(ctx context.Context, devID string) (model.Device, error)
	DeployConfiguration(ctx context.Context, device model.Device, request model.DeployConfigurationRequest) (model.DeployConfigurationResponse, error)
}

// app is an app object
type app struct {
	store store.DataStore
	Config
	workflows client.WorkflowsOtherAPI
}

type Config struct {
	HaveAuditLogs bool
}

// NewApp initialize a new deviceconfig App
func New(ds store.DataStore, wf client.WorkflowsOtherAPI, config ...Config) App {
	conf := Config{}
	for _, cfgIn := range config {
		if cfgIn.HaveAuditLogs {
			conf.HaveAuditLogs = true
		}
	}
	return &app{
		store:     ds,
		workflows: wf,
		Config:    conf,
	}
}

// HealthCheck performs a health check and returns an error if it fails
func (a *app) HealthCheck(ctx context.Context) error {
	return a.store.Ping(ctx)
}

func (a *app) ProvisionTenant(ctx context.Context, tenant model.NewTenant) error {
	ctx = identity.WithContext(ctx, &identity.Identity{
		Tenant: tenant.TenantID,
	})
	return a.store.MigrateLatest(ctx)
}

func (d *app) DeleteTenant(ctx context.Context, tenant_id string) error {
	tenantCtx := identity.WithContext(ctx, &identity.Identity{
		Tenant: tenant_id,
	})
	return d.store.DeleteTenant(tenantCtx, tenant_id)
}

func (a *app) ProvisionDevice(ctx context.Context, dev model.NewDevice) error {
	now := time.Now()
	return a.store.InsertDevice(ctx, model.Device{
		ID:        dev.ID,
		UpdatedTS: &now,
	})
}

func (a *app) DecommissionDevice(ctx context.Context, devID string) error {
	return a.store.DeleteDevice(ctx, devID)
}

func (a *app) SubmitAuditLog(ctx context.Context, log model.AuditLog) error {
	const workflowSubmitAuditLog = "emit_auditlog"
	id := identity.FromContext(ctx)
	_, _, err := a.workflows.StartWorkflow(ctx, workflowSubmitAuditLog).
		RequestBody(map[string]any{
			"auditlog":   log,
			"tenant_id":  id.Tenant,
			"request_id": requestid.FromContext(ctx),
		}).
		Execute()
	return err
}

func (a *app) deployConfiguration(ctx context.Context,
	tenantID, deviceID string,
	deploymentID uuid.UUID,
	configuration []byte,
	retries uint,
	updateControlMap map[string]any,
) error {
	const workflowDeployConfig = "deploy_device_configuration"
	id := identity.FromContext(ctx)
	req := map[string]any{
		"tenant_id":     id.Tenant,
		"request_id":    requestid.FromContext(ctx),
		`device_id`:     deviceID,
		`deployment_id`: deploymentID,
		`configuration`: json.RawMessage(configuration),
		`retries`:       retries,
	}
	if updateControlMap != nil {
		req["update_control_map"] = updateControlMap
	}
	_, _, err := a.workflows.StartWorkflow(ctx, workflowDeployConfig).
		RequestBody(req).
		Execute()
	return err
}

func (a *app) SetConfiguration(ctx context.Context,
	devID string,
	configuration model.Attributes) error {
	now := time.Now()
	err := a.store.ReplaceConfiguration(ctx, model.Device{
		ID:                   devID,
		ConfiguredAttributes: configuration,
		UpdatedTS:            &now,
	})
	if err != nil {
		return err
	}
	if identity := identity.FromContext(ctx); identity != nil &&
		identity.IsUser && a.HaveAuditLogs {
		userID := identity.Subject
		configuration, err := configuration.MarshalJSON()
		if err == nil {
			err := a.SubmitAuditLog(ctx, model.AuditLog{
				Action: model.ActionSetConfiguration,
				Actor: model.AuditLogActor{
					ID:   userID,
					Type: model.ActorUser,
				},
				Object: model.AuditLogObject{
					ID:   devID,
					Type: model.ObjectDevice,
				},
				Change:  string(configuration),
				EventTS: time.Now(),
			})
			if err != nil {
				return errors.Wrap(err,
					"failed to submit audit log for setting the device configuration",
				)
			}
		}
	}

	return nil
}

func (a *app) UpdateConfiguration(
	ctx context.Context,
	devID string,
	attrs model.Attributes,
) error {
	err := a.store.UpdateConfiguration(ctx, devID, attrs)
	if err != nil {
		return err
	}
	if identity := identity.FromContext(ctx); identity != nil &&
		identity.IsUser && a.HaveAuditLogs {
		userID := identity.Subject
		configuration, err := attrs.MarshalJSON()
		if err == nil {
			err = a.SubmitAuditLog(ctx, model.AuditLog{
				Action: model.ActionSetConfiguration,
				Actor: model.AuditLogActor{
					ID:   userID,
					Type: model.ActorUser,
				},
				Object: model.AuditLogObject{
					ID:   devID,
					Type: model.ObjectDevice,
				},
				Change:  string(configuration),
				EventTS: time.Now(),
			})
		}
		if err != nil {
			return errors.Wrap(err,
				"failed to submit audit log for updating the device configuration",
			)
		}
	}
	return nil
}

func (a *app) SetReportedConfiguration(ctx context.Context,
	devID string,
	configuration model.Attributes) error {
	now := time.Now()
	return a.store.ReplaceReportedConfiguration(ctx, model.Device{
		ID:                 devID,
		ReportedAttributes: configuration,
		ReportTS:           &now,
	})
}

func (a *app) GetDevice(ctx context.Context, devID string) (model.Device, error) {
	return a.store.GetDevice(ctx, devID)
}

func (a *app) DeployConfiguration(ctx context.Context, device model.Device,
	request model.DeployConfigurationRequest) (model.DeployConfigurationResponse, error) {
	response := model.DeployConfigurationResponse{}
	configuration, err := device.ConfiguredAttributes.MarshalJSON()
	if err != nil {
		return response, err
	}
	identity := identity.FromContext(ctx)
	if identity == nil {
		return response, errors.New("identity missing from the context")
	}
	deploymentID := uuid.New()
	err = a.store.SetDeploymentID(ctx, device.ID, deploymentID)
	if err != nil {
		return response, nil
	}
	response.DeploymentID = deploymentID
	err = a.deployConfiguration(ctx, identity.Tenant, device.ID,
		response.DeploymentID, configuration, request.Retries, request.UpdateControlMap)
	if err != nil {
		return response, err
	}
	if a.HaveAuditLogs {
		userID := identity.Subject
		err = a.SubmitAuditLog(ctx, model.AuditLog{
			Action: model.ActionDeployConfiguration,
			Actor: model.AuditLogActor{
				ID:   userID,
				Type: model.ActorUser,
			},
			Object: model.AuditLogObject{
				ID:   device.ID,
				Type: model.ObjectDevice,
			},
			Change:  string(configuration),
			EventTS: time.Now(),
		})
		if err != nil {
			return response, errors.Wrap(err,
				"failed to submit audit log for deploying the device configuration",
			)
		}
	}
	return response, nil
}
