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

package bootstrap

import (
	"context"
	"crypto/aes"
	"crypto/cipher"
	"encoding/hex"

	"github.com/absmach/magistrala"
	smqauthn "github.com/absmach/magistrala/pkg/authn"
	"github.com/absmach/magistrala/pkg/errors"
	repoerr "github.com/absmach/magistrala/pkg/errors/repository"
	svcerr "github.com/absmach/magistrala/pkg/errors/service"
	mgsdk "github.com/absmach/magistrala/pkg/sdk"
)

var (
	// ErrExternalKey indicates a non-existent bootstrap configuration for given external key.
	ErrExternalKey = errors.NewAuthZError("failed to get bootstrap configuration for given external key")

	// ErrExternalKeySecure indicates error in getting bootstrap configuration for given encrypted external key.
	ErrExternalKeySecure = errors.NewAuthZError("failed to get bootstrap configuration for given encrypted external key")

	// ErrBootstrap indicates error in getting bootstrap configuration.
	ErrBootstrap = errors.New("failed to read bootstrap configuration")

	// ErrAddBootstrap indicates error in adding bootstrap configuration.
	ErrAddBootstrap = errors.NewServiceError("failed to add bootstrap configuration")

	// ErrBootstrapStatus indicates an invalid bootstrap status.
	ErrBootstrapStatus = errors.NewRequestError("invalid bootstrap status")

	errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
	errEnableConfig    = errors.New("failed to enable bootstrap configuration")
	errDisableConfig   = errors.New("failed to disable bootstrap configuration")
	errUpdateCert      = errors.New("failed to update cert")

	errCreateProfile   = errors.New("failed to create profile")
	errViewProfile     = errors.New("failed to view profile")
	errUpdateProfile   = errors.New("failed to update profile")
	errDeleteProfile   = errors.New("failed to delete profile")
	errListProfiles    = errors.New("failed to list profiles")
	errAssignProfile   = errors.New("failed to assign profile to enrollment")
	errBindResources   = errors.New("failed to bind resources")
	errListBindings    = errors.New("failed to list bindings")
	errRefreshBinding  = errors.New("failed to refresh bindings")
	errRenderBootstrap = errors.New("failed to render bootstrap configuration")
)

var _ Service = (*bootstrapService)(nil)

// Service specifies an API that must be fulfilled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
type Service interface {
	// Add adds new Client Config to the user identified by the provided token.
	Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error)

	// View returns Client Config with given ID belonging to the user identified by the given token.
	View(ctx context.Context, session smqauthn.Session, id string) (Config, error)

	// Update updates editable fields of the provided Config.
	Update(ctx context.Context, session smqauthn.Session, cfg Config) error

	// UpdateCert updates an existing Config certificate and token.
	// A non-nil error is returned to indicate operation failure.
	UpdateCert(ctx context.Context, session smqauthn.Session, id, clientCert, clientKey, caCert string) (Config, error)

	// List returns subset of Configs with given search params that belong to the
	// user identified by the given token.
	List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error)

	// Remove removes Config with specified token that belongs to the user identified by the given token.
	Remove(ctx context.Context, session smqauthn.Session, id string) error

	// Bootstrap returns Config to the Client with provided external ID using external key.
	Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error)

	// EnableConfig enables the Config so its device can successfully bootstrap.
	EnableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error)

	// DisableConfig disables the Config, preventing its device from bootstrapping.
	DisableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error)

	// CreateProfile persists a new device Profile.
	CreateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error)

	// ViewProfile returns the Profile with the given ID.
	ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (Profile, error)

	// UpdateProfile updates editable fields of the given Profile and returns the updated Profile.
	UpdateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error)

	// ListProfiles returns a page of Profiles belonging to the domain.
	ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (ProfilesPage, error)

	// DeleteProfile removes the Profile with the given ID.
	DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error

	// AssignProfile sets the ProfileID on an existing enrollment (Config).
	AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error

	// BindResources resolves the requested bindings through their owning services,
	// stores snapshots, and marks the enrollment renderable when all required slots
	// are satisfied.
	BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []BindingRequest) error

	// ListBindings returns all stored binding snapshots for an enrollment.
	ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]BindingSnapshot, error)

	// RefreshBindings re-resolves all existing bindings for an enrollment and
	// updates the stored snapshots.
	RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error
}

// ConfigReader is used to parse Config into format which will be encoded
// as a JSON and consumed from the client side. The purpose of this interface
// is to provide convenient way to generate custom configuration response
// based on the specific Config which will be consumed by the client.
type ConfigReader interface {
	ReadConfig(Config, bool) (any, error)
}

type bootstrapService struct {
	configs    ConfigRepository
	profiles   ProfileRepository
	bindings   BindingStore
	resolver   BindingResolver
	renderer   Renderer
	hasher     Hasher
	sdk        mgsdk.SDK
	encKey     []byte
	idProvider magistrala.IDProvider
}

// New returns new Bootstrap service.
func New(
	configs ConfigRepository,
	profiles ProfileRepository,
	bindings BindingStore,
	resolver BindingResolver,
	renderer Renderer,
	sdk mgsdk.SDK,
	hasher Hasher,
	encKey []byte,
	idp magistrala.IDProvider,
) Service {
	return &bootstrapService{
		configs:    configs,
		profiles:   profiles,
		bindings:   bindings,
		resolver:   resolver,
		renderer:   renderer,
		hasher:     hasher,
		sdk:        sdk,
		encKey:     encKey,
		idProvider: idp,
	}
}

func (bs bootstrapService) Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error) {
	id, err := bs.idProvider.ID()
	if err != nil {
		return Config{}, errors.Wrap(ErrAddBootstrap, err)
	}

	hashedKey, err := bs.hasher.Hash(cfg.ExternalKey)
	if err != nil {
		return Config{}, errors.Wrap(ErrAddBootstrap, err)
	}

	cfg.ID = id
	cfg.DomainID = session.DomainID
	cfg.Status = Active
	cfg.ExternalKey = hashedKey

	saved, err := bs.configs.Save(ctx, cfg)
	if err != nil {
		if errors.Contains(err, repoerr.ErrConflict) {
			return Config{}, errors.Wrap(svcerr.ErrConflict, err)
		}
		return Config{}, errors.Wrap(ErrAddBootstrap, err)
	}

	cfg.ID = saved
	return cfg, nil
}

func (bs bootstrapService) View(ctx context.Context, session smqauthn.Session, id string) (Config, error) {
	cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
	if err != nil {
		return Config{}, errors.Wrap(svcerr.ErrViewEntity, err)
	}
	return cfg, nil
}

func (bs bootstrapService) Update(ctx context.Context, session smqauthn.Session, cfg Config) error {
	cfg.DomainID = session.DomainID
	if err := bs.configs.Update(ctx, cfg); err != nil {
		return errors.Wrap(svcerr.ErrUpdateEntity, err)
	}
	return nil
}

func (bs bootstrapService) UpdateCert(ctx context.Context, session smqauthn.Session, id, clientCert, clientKey, caCert string) (Config, error) {
	cfg, err := bs.configs.UpdateCert(ctx, session.DomainID, id, clientCert, clientKey, caCert)
	if err != nil {
		return Config{}, errors.Wrap(errUpdateCert, err)
	}
	return cfg, nil
}

func (bs bootstrapService) List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error) {
	return bs.configs.RetrieveAll(ctx, session.DomainID, filter, offset, limit), nil
}

func (bs bootstrapService) Remove(ctx context.Context, session smqauthn.Session, id string) error {
	if err := bs.configs.Remove(ctx, session.DomainID, id); err != nil {
		return errors.Wrap(errRemoveBootstrap, err)
	}
	return nil
}

func (bs bootstrapService) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error) {
	cfg, err := bs.configs.RetrieveByExternalID(ctx, externalID)
	if err != nil {
		return cfg, errors.Wrap(ErrBootstrap, err)
	}
	if secure {
		dec, err := bs.dec(externalKey)
		if err != nil {
			return Config{}, errors.Wrap(ErrExternalKeySecure, err)
		}
		externalKey = dec
	}

	if err := bs.hasher.Compare(externalKey, cfg.ExternalKey); err != nil {
		return Config{}, ErrExternalKey
	}
	if cfg.Status == DisabledStatus {
		return Config{}, ErrBootstrap
	}

	cfg, err = bs.renderBootstrapConfig(ctx, cfg)
	if err != nil {
		return Config{}, errors.Wrap(ErrBootstrap, err)
	}

	return cfg, nil
}

func (bs bootstrapService) renderBootstrapConfig(ctx context.Context, cfg Config) (Config, error) {
	if cfg.ProfileID == "" {
		return cfg, nil
	}
	if bs.profiles == nil || bs.bindings == nil || bs.renderer == nil {
		return Config{}, errors.Wrap(errRenderBootstrap, errors.New("profile rendering support not configured"))
	}

	profile, err := bs.profiles.RetrieveByID(ctx, cfg.DomainID, cfg.ProfileID)
	if err != nil {
		return Config{}, errors.Wrap(errRenderBootstrap, err)
	}

	bindings, err := bs.bindings.Retrieve(ctx, cfg.ID)
	if err != nil {
		return Config{}, errors.Wrap(errRenderBootstrap, err)
	}
	if err := validateRequiredBindings(profile, bindings); err != nil {
		return Config{}, errors.Wrap(errRenderBootstrap, err)
	}
	bindings, err = bs.decryptSecretSnapshots(bindings)
	if err != nil {
		return Config{}, errors.Wrap(errRenderBootstrap, err)
	}

	rendered, err := bs.renderer.Render(profile, cfg, bindings)
	if err != nil {
		return Config{}, errors.Wrap(errRenderBootstrap, err)
	}

	cfg.Content = string(rendered)
	return cfg, nil
}

func (bs bootstrapService) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error) {
	cfg, err := bs.changeConfigStatus(ctx, session.DomainID, id, EnabledStatus)
	if err != nil {
		return Config{}, errors.Wrap(errEnableConfig, err)
	}
	return cfg, nil
}

func (bs bootstrapService) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error) {
	cfg, err := bs.changeConfigStatus(ctx, session.DomainID, id, DisabledStatus)
	if err != nil {
		return Config{}, errors.Wrap(errDisableConfig, err)
	}
	return cfg, nil
}

func (bs bootstrapService) changeConfigStatus(ctx context.Context, domainID, id string, status Status) (Config, error) {
	cfg, err := bs.configs.RetrieveByID(ctx, domainID, id)
	if err != nil {
		return Config{}, errors.Wrap(svcerr.ErrViewEntity, err)
	}
	if cfg.Status == status {
		return cfg, nil
	}
	if err := bs.configs.ChangeStatus(ctx, domainID, id, status); err != nil {
		return Config{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
	}
	cfg.Status = status
	return cfg, nil
}

// --- Profile management ---

func (bs bootstrapService) CreateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error) {
	if bs.profiles == nil {
		return Profile{}, errors.Wrap(errCreateProfile, errors.New("profile repository not configured"))
	}
	id, err := bs.idProvider.ID()
	if err != nil {
		return Profile{}, errors.Wrap(errCreateProfile, err)
	}
	p.ID = id
	p.DomainID = session.DomainID
	if p.ContentFormat == "" {
		p.ContentFormat = ContentFormatJSON
	}
	p.Version = 1
	if err := validateProfileBindingSlots(p); err != nil {
		return Profile{}, errors.Wrap(errCreateProfile, err)
	}
	if err := validateProfileTemplate(p); err != nil {
		return Profile{}, errors.Wrap(errCreateProfile, err)
	}
	saved, err := bs.profiles.Save(ctx, p)
	if err != nil {
		return Profile{}, errors.Wrap(errCreateProfile, err)
	}
	return saved, nil
}

func (bs bootstrapService) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (Profile, error) {
	if bs.profiles == nil {
		return Profile{}, errors.Wrap(errViewProfile, errors.New("profile repository not configured"))
	}
	p, err := bs.profiles.RetrieveByID(ctx, session.DomainID, profileID)
	if err != nil {
		return Profile{}, errors.Wrap(errViewProfile, err)
	}
	return p, nil
}

func (bs bootstrapService) UpdateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error) {
	if bs.profiles == nil {
		return Profile{}, errors.Wrap(errUpdateProfile, errors.New("profile repository not configured"))
	}
	p.DomainID = session.DomainID
	if err := validateProfileBindingSlots(p); err != nil {
		return Profile{}, errors.Wrap(errUpdateProfile, err)
	}
	if err := validateProfileTemplate(p); err != nil {
		return Profile{}, errors.Wrap(errUpdateProfile, err)
	}
	updated, err := bs.profiles.Update(ctx, p)
	if err != nil {
		return Profile{}, errors.Wrap(errUpdateProfile, err)
	}
	return updated, nil
}

func (bs bootstrapService) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (ProfilesPage, error) {
	if bs.profiles == nil {
		return ProfilesPage{}, errors.Wrap(errListProfiles, errors.New("profile repository not configured"))
	}
	page, err := bs.profiles.RetrieveAll(ctx, session.DomainID, offset, limit, name)
	if err != nil {
		return ProfilesPage{}, errors.Wrap(errListProfiles, err)
	}
	return page, nil
}

func (bs bootstrapService) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error {
	if bs.profiles == nil {
		return errors.Wrap(errDeleteProfile, errors.New("profile repository not configured"))
	}
	if err := bs.profiles.Delete(ctx, session.DomainID, profileID); err != nil {
		return errors.Wrap(errDeleteProfile, err)
	}
	return nil
}

// --- Enrollment-profile assignment ---

func (bs bootstrapService) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error {
	if bs.profiles == nil {
		return errors.Wrap(errAssignProfile, errors.New("profile repository not configured"))
	}
	// Validate profile exists in domain.
	if _, err := bs.profiles.RetrieveByID(ctx, session.DomainID, profileID); err != nil {
		return errors.Wrap(errAssignProfile, err)
	}
	if err := bs.configs.AssignProfile(ctx, session.DomainID, configID, profileID); err != nil {
		return errors.Wrap(errAssignProfile, err)
	}
	return nil
}

// --- Binding management ---

func (bs bootstrapService) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, requested []BindingRequest) error {
	if bs.profiles == nil || bs.bindings == nil || bs.resolver == nil {
		return errors.Wrap(errBindResources, errors.New("binding support not configured"))
	}
	cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, configID)
	if err != nil {
		return errors.Wrap(errBindResources, err)
	}
	profile, err := bs.profiles.RetrieveByID(ctx, session.DomainID, cfg.ProfileID)
	if err != nil {
		return errors.Wrap(errBindResources, err)
	}
	if err := validateRequestedBindings(profile, requested); err != nil {
		return errors.Wrap(errBindResources, err)
	}
	snapshots, err := bs.resolver.Resolve(ctx, ResolveRequest{
		Enrollment: cfg,
		Token:      token,
		Requested:  requested,
	})
	if err != nil {
		return errors.Wrap(errBindResources, err)
	}
	existing, err := bs.bindings.Retrieve(ctx, configID)
	if err != nil {
		return errors.Wrap(errBindResources, err)
	}
	if err := validateRequiredBindings(profile, mergeBindingSnapshots(existing, snapshots)); err != nil {
		return errors.Wrap(errBindResources, err)
	}
	snapshots, err = bs.encryptSecretSnapshots(snapshots)
	if err != nil {
		return errors.Wrap(errBindResources, err)
	}
	if err := bs.bindings.Save(ctx, configID, snapshots); err != nil {
		return errors.Wrap(errBindResources, err)
	}
	return nil
}

func (bs bootstrapService) ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]BindingSnapshot, error) {
	if bs.bindings == nil {
		return nil, errors.Wrap(errListBindings, errors.New("binding support not configured"))
	}
	if _, err := bs.configs.RetrieveByID(ctx, session.DomainID, configID); err != nil {
		return nil, errors.Wrap(errListBindings, err)
	}
	snapshots, err := bs.bindings.Retrieve(ctx, configID)
	if err != nil {
		return nil, errors.Wrap(errListBindings, err)
	}
	return hideSecretSnapshots(snapshots), nil
}

func (bs bootstrapService) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error {
	if bs.profiles == nil || bs.bindings == nil || bs.resolver == nil {
		return errors.Wrap(errRefreshBinding, errors.New("binding support not configured"))
	}
	cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, configID)
	if err != nil {
		return errors.Wrap(errRefreshBinding, err)
	}
	profile, err := bs.profiles.RetrieveByID(ctx, session.DomainID, cfg.ProfileID)
	if err != nil {
		return errors.Wrap(errRefreshBinding, err)
	}
	existing, err := bs.bindings.Retrieve(ctx, configID)
	if err != nil {
		return errors.Wrap(errRefreshBinding, err)
	}
	if len(existing) == 0 {
		return nil
	}
	// Re-resolve every existing binding to refresh its snapshot.
	requested := make([]BindingRequest, len(existing))
	for i, b := range existing {
		requested[i] = BindingRequest{Slot: b.Slot, Type: b.Type, ResourceID: b.ResourceID}
	}
	if err := validateRequestedBindings(profile, requested); err != nil {
		return errors.Wrap(errRefreshBinding, err)
	}
	refreshed, err := bs.resolver.Resolve(ctx, ResolveRequest{
		Enrollment: cfg,
		Token:      token,
		Requested:  requested,
	})
	if err != nil {
		return errors.Wrap(errRefreshBinding, err)
	}
	if err := validateRequiredBindings(profile, refreshed); err != nil {
		return errors.Wrap(errRefreshBinding, err)
	}
	refreshed, err = bs.encryptSecretSnapshots(refreshed)
	if err != nil {
		return errors.Wrap(errRefreshBinding, err)
	}
	return bs.bindings.Save(ctx, configID, refreshed)
}

func (bs bootstrapService) dec(in string) (string, error) {
	ciphertext, err := hex.DecodeString(in)
	if err != nil {
		return "", err
	}
	block, err := aes.NewCipher(bs.encKey)
	if err != nil {
		return "", err
	}
	if len(ciphertext) < aes.BlockSize {
		return "", err
	}
	iv := ciphertext[:aes.BlockSize]
	ciphertext = ciphertext[aes.BlockSize:]
	stream := cipher.NewCFBDecrypter(block, iv)
	stream.XORKeyStream(ciphertext, ciphertext)
	return string(ciphertext), nil
}
