package database

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/crowdsecurity/crowdsec/pkg/database/ent"
	"github.com/crowdsecurity/crowdsec/pkg/database/ent/bouncer"
	"github.com/crowdsecurity/crowdsec/pkg/models"
)

type BouncerNotFoundError struct {
	BouncerName string
}

func (e *BouncerNotFoundError) Error() string {
	return fmt.Sprintf("'%s' does not exist", e.BouncerName)
}

func (c *Client) BouncerUpdateBaseMetrics(ctx context.Context, bouncerName string, bouncerType string, baseMetrics models.BaseMetrics) error {
	os := baseMetrics.Os
	features := strings.Join(baseMetrics.FeatureFlags, ",")

	_, err := c.Ent.Bouncer.
		Update().
		Where(bouncer.NameEQ(bouncerName)).
		SetNillableVersion(baseMetrics.Version).
		SetOsname(*os.Name).
		SetOsfamily(os.Family).
		SetOsversion(*os.Version).
		SetFeatureflags(features).
		SetType(bouncerType).
		Save(ctx)
	if err != nil {
		return fmt.Errorf("unable to update base bouncer metrics in database: %w", err)
	}

	return nil
}

func (c *Client) SelectBouncers(ctx context.Context, apiKeyHash string, authType string) ([]*ent.Bouncer, error) {
	// Order by ID so manually created bouncer will be first in the list to use as the base name
	// when automatically creating a new entry if API keys are shared
	result, err := c.Ent.Bouncer.Query().Where(bouncer.APIKeyEQ(apiKeyHash), bouncer.AuthTypeEQ(authType)).Order(ent.Asc(bouncer.FieldID)).All(ctx)
	if err != nil {
		return nil, err
	}

	return result, nil
}

func (c *Client) SelectBouncerWithIP(ctx context.Context, apiKeyHash string, clientIP string) (*ent.Bouncer, error) {
	result, err := c.Ent.Bouncer.Query().Where(bouncer.APIKeyEQ(apiKeyHash), bouncer.IPAddressEQ(clientIP)).First(ctx)
	if err != nil {
		return nil, err
	}

	return result, nil
}

func (c *Client) SelectBouncerByName(ctx context.Context, bouncerName string) (*ent.Bouncer, error) {
	result, err := c.Ent.Bouncer.Query().Where(bouncer.NameEQ(bouncerName)).First(ctx)
	if err != nil {
		return nil, err
	}

	return result, nil
}

func (c *Client) ListBouncers(ctx context.Context) ([]*ent.Bouncer, error) {
	result, err := c.Ent.Bouncer.Query().All(ctx)
	if err != nil {
		return nil, fmt.Errorf("listing bouncers: %w: %w", err, QueryFail)
	}

	return result, nil
}

func (c *Client) CreateBouncer(ctx context.Context, name string, ipAddr string, apiKey string, authType string, autoCreated bool) (*ent.Bouncer, error) {
	bouncer, err := c.Ent.Bouncer.
		Create().
		SetName(name).
		SetAPIKey(apiKey).
		SetRevoked(false).
		SetAuthType(authType).
		SetIPAddress(ipAddr).
		SetAutoCreated(autoCreated).
		Save(ctx)
	if err != nil {
		if ent.IsConstraintError(err) {
			return nil, fmt.Errorf("bouncer %s already exists", name)
		}

		return nil, fmt.Errorf("unable to create bouncer: %w", err)
	}

	return bouncer, nil
}

func (c *Client) DeleteBouncer(ctx context.Context, name string) error {
	nbDeleted, err := c.Ent.Bouncer.
		Delete().
		Where(bouncer.NameEQ(name)).
		Exec(ctx)
	if err != nil {
		return err
	}

	if nbDeleted == 0 {
		return &BouncerNotFoundError{BouncerName: name}
	}

	return nil
}

func (c *Client) BulkDeleteBouncers(ctx context.Context, bouncers []*ent.Bouncer) (int, error) {
	ids := make([]int, len(bouncers))
	for i, b := range bouncers {
		ids[i] = b.ID
	}

	nbDeleted, err := c.Ent.Bouncer.Delete().Where(bouncer.IDIn(ids...)).Exec(ctx)
	if err != nil {
		return nbDeleted, fmt.Errorf("unable to delete bouncers: %w", err)
	}

	return nbDeleted, nil
}

func (c *Client) UpdateBouncerLastPull(ctx context.Context, lastPull time.Time, id int) error {
	_, err := c.Ent.Bouncer.UpdateOneID(id).
		SetLastPull(lastPull).
		Save(ctx)
	if err != nil {
		return fmt.Errorf("unable to update machine last pull in database: %w", err)
	}

	return nil
}

func (c *Client) UpdateBouncerIP(ctx context.Context, ipAddr string, id int) error {
	_, err := c.Ent.Bouncer.UpdateOneID(id).SetIPAddress(ipAddr).Save(ctx)
	if err != nil {
		return fmt.Errorf("unable to update bouncer ip address in database: %w", err)
	}

	return nil
}

func (c *Client) UpdateBouncerTypeAndVersion(ctx context.Context, bType string, version string, id int) error {
	_, err := c.Ent.Bouncer.UpdateOneID(id).SetVersion(version).SetType(bType).Save(ctx)
	if err != nil {
		return fmt.Errorf("unable to update bouncer type and version in database: %w", err)
	}

	return nil
}

func (c *Client) QueryBouncersInactiveSince(ctx context.Context, t time.Time) ([]*ent.Bouncer, error) {
	return c.Ent.Bouncer.Query().Where(
		// poor man's coalesce
		bouncer.Or(
			bouncer.LastPullLT(t),
			bouncer.And(
				bouncer.LastPullIsNil(),
				bouncer.CreatedAtLT(t),
			),
		),
	).All(ctx)
}
