package integration

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"

	"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache"
	cache2 "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache"
	"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database"
	dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
)

type Client interface {
	HandleVacancyResponse(request HandleVacancy) error
}

type (
	CompanySecrets any

	client struct {
		cache   cache2.Client
		db      database.Client
		logger  *slog.Logger
		secrets map[string]CompanySecrets
	}

	Config struct {
		Cache   cache2.Client
		Db      database.Client
		Logger  *slog.Logger
		Secrets map[string]CompanySecrets
	}
)

func New(c Config) (Client, error) {
	return &client{
		cache:   c.Cache,
		db:      c.Db,
		logger:  c.Logger,
		secrets: c.Secrets,
	}, nil
}

// TODO: need to think about possible errors
// e.g. cache is down, connection lost etc.

func (c *client) HandleVacancyResponse(request HandleVacancy) error {
	const handlerName = "integration.HandleVacancyResponse"

	companyId, vacancyExtraFields, err := c.getVacancyIntegrationInfo(context.Background(), request.VacancyId)
	if err != nil {
		return err
	}

	if vacancyExtraFields == nil {
		c.logger.Debug("vacancy extra fields are empty",
			slog.String("vacancy_id", request.VacancyId),
			slog.String("handler", handlerName),
		)

		return nil
	}

	companyMetadata, companyExtraFieldsTemplate, err := c.getCompanyIntegrationInfo(context.Background(), companyId)
	if err != nil {
		return err
	}

	if companyMetadata == nil {
		c.logger.Debug("company metadata is empty",
			slog.String("company_id", companyId),
			slog.String("handler", handlerName),
		)

		return nil
	}

	if companyExtraFieldsTemplate == nil {
		c.logger.Debug("company extra fields template are empty",
			slog.String("company_id", companyId),
			slog.String("handler", handlerName),
		)

		return nil
	}

	companyName, ok := (*companyMetadata)[companyNameMetadataKey]
	if !ok {
		return fmt.Errorf("company name not found in metadata")
	}

	switch companyName {
	case VkusvillCompanyName:
		return c.handleVkusvillIntegration(
			context.Background(),
			request,
			vacancyExtraFields,
			companyMetadata,
			companyExtraFieldsTemplate,
		)

	default:
		return ErrUnknownIntegration
	}
}

//nolint:gocognit // not that hard
func (c *client) getVacancyIntegrationInfo(
	ctx context.Context,
	vacancyId string,
) (string, *VacancyExtraFields, error) {
	const handlerName = "integration.getVacancyIntegrationInfo"

	var (
		companyId          string
		vacancyExtraFields VacancyExtraFields
		gotCached          = true
	)

	// get company id and vacancy filled template
	cachedIntegrationInfo, err := c.cache.Get(context.Background(), vacancyId, cache.IntegrationVacancyValueType)
	if err != nil && !errors.Is(err, cache.ErrKeyNotFound) {
		if err := tryRepeat(context.Background(), func() error {
			cachedIntegrationInfo, err = c.cache.Get(context.Background(), vacancyId, cache.IntegrationCompanyValueType)
			return err
		}, defaultRetryAmount); err != nil {
			// TODO: maybe we should process further and try DB instead
			// Need to handle the error deeper
			// e.g. if cache is down we can try to get info from DB
			// on the other hand, if there is any other fatal error, we should return it
			return "", nil, fmt.Errorf("error getting vacancy integration info from cache: %w", err)
		}
	}

	if errors.Is(err, cache.ErrKeyNotFound) {
		// try get data from DB
		dbCompanyId, dbFields, err := c.db.GetVacancyIntegrationInfoById(context.Background(), vacancyId)
		if err != nil {
			if errors.Is(err, dberrors.ErrNotFound) {
				return "", nil, ErrVacancyNotFound
			}

			ctx, cancel := context.WithTimeout(context.Background(), defaultRetryTimeout)
			defer cancel()

			if err := tryRepeat(ctx, func() error {
				dbCompanyId, dbFields, err = c.db.GetVacancyIntegrationInfoById(context.Background(), vacancyId)
				return err
			}, 0); err != nil {
				return "", nil, fmt.Errorf("error getting vacancy integration info from DB: %w", err)
			}
		}

		companyId = dbCompanyId
		vacancyExtraFields = *new(VacancyExtraFields).From(dbFields)
		gotCached = false

		// store obtained info in cache
		fullMetadata := VacancyIntegrationInfo{
			CompanyId:   companyId,
			ExtraFields: vacancyExtraFields,
		}

		jsonMetadata, err := json.Marshal(fullMetadata)
		if err != nil {
			c.logger.Error("error marshalling vacancy integration info",
				slog.String("error", err.Error()),
				slog.String("vacancy_id", vacancyId),
				slog.String("handler", handlerName))
		} else {
			if err := tryRepeat(context.Background(), func() error {
				return c.cache.Set(context.Background(), vacancyId, cache.IntegrationVacancyValueType, string(jsonMetadata), 0)
			}, defaultRetryAmount); err != nil {
				c.logger.Error("error saving vacancy integration info to cache",
					slog.String("error", err.Error()),
					slog.String("vacancy_id", vacancyId),
					slog.String("handler", handlerName))
			}
		}
	}

	if gotCached {
		var fullMetadata VacancyIntegrationInfo
		if err := json.Unmarshal([]byte(cachedIntegrationInfo), &fullMetadata); err != nil {
			return "", nil, fmt.Errorf("error unmarshalling cached metadata: %w", err)
		}

		companyId = fullMetadata.CompanyId
		vacancyExtraFields = fullMetadata.ExtraFields
	}

	return companyId, &vacancyExtraFields, nil
}

//nolint:gocognit // not that hard
func (c *client) getCompanyIntegrationInfo(
	ctx context.Context,
	companyId string,
) (*CompanyMetadata, *CompanyExtraFieldsTemplate, error) {
	const handlerName = "integration.getCompanyIntegrationInfo"

	var (
		companyMetadata       CompanyMetadata
		companyFieldsTemplate CompanyExtraFieldsTemplate
		gotCached             = true
	)

	// get company metadata and template
	cachedIntegrationInfo, err := c.cache.Get(context.Background(), companyId, cache.IntegrationCompanyValueType)
	if err != nil && !errors.Is(err, cache.ErrKeyNotFound) {
		if err := tryRepeat(context.Background(), func() error {
			cachedIntegrationInfo, err = c.cache.Get(context.Background(), companyId, cache.IntegrationCompanyValueType)
			return err
		}, defaultRetryAmount); err != nil {
			// TODO: maybe we should process further and try DB instead
			// Need to handle the error deeper
			// e.g. if cache is down we can try to get info from DB
			// on the other hand, if there is any other fatal error, we should return it
			return nil, nil, fmt.Errorf("error getting company metadata from cache: %w", err)
		}
	}

	if errors.Is(err, cache.ErrKeyNotFound) {
		// try get data from DB
		dbMetadata, dbFieldsTemplate, err := c.db.GetCompanyMetadataById(context.Background(), companyId)
		if err != nil {
			if errors.Is(err, dberrors.ErrNotFound) {
				return nil, nil, ErrCompanyNotFound
			}

			// TODO: think about other retry policies
			ctx, cancel := context.WithTimeout(context.Background(), defaultRetryTimeout)
			defer cancel()

			if err := tryRepeat(ctx, func() error {
				dbMetadata, dbFieldsTemplate, err = c.db.GetCompanyMetadataById(context.Background(), companyId)
				return err
			}, 0); err != nil {
				return nil, nil, fmt.Errorf("error getting company integration info from DB: %w", err)
			}
		}

		companyMetadata = *new(CompanyMetadata).From(dbMetadata)
		companyFieldsTemplate = *new(CompanyExtraFieldsTemplate).From(dbFieldsTemplate)
		gotCached = false

		// store obtained info in cache
		fullMetadata := CompanyIntegrationInfo{
			Metadata:            companyMetadata,
			ExtraFieldsTemplate: companyFieldsTemplate,
		}

		jsonMetadata, err := json.Marshal(fullMetadata)
		if err != nil {
			c.logger.Error("error marshalling company integration info",
				slog.String("error", err.Error()),
				slog.String("company_id", companyId),
				slog.String("handler", handlerName))
		} else {
			if err := tryRepeat(context.Background(), func() error {
				return c.cache.Set(context.Background(), companyId, cache.IntegrationCompanyValueType, string(jsonMetadata), 0)
			}, defaultRetryAmount); err != nil {
				c.logger.Error("error saving company integration info to cache",
					slog.String("error", err.Error()),
					slog.String("company_id", companyId),
					slog.String("handler", handlerName))
			}
		}
	}

	if gotCached {
		var fullMetadata CompanyIntegrationInfo
		if err := json.Unmarshal([]byte(cachedIntegrationInfo), &fullMetadata); err != nil {
			return nil, nil, fmt.Errorf("error unmarshalling cached metadata: %w", err)
		}

		companyMetadata = fullMetadata.Metadata
		companyFieldsTemplate = fullMetadata.ExtraFieldsTemplate
	}

	return &companyMetadata, &companyFieldsTemplate, nil
}

// tryRepeat tries to execute function N times.
// If number of attempts is 0, the function will be executed until context is canceled
func tryRepeat(ctx context.Context, fn func() error, attempts uint) error {
	ch := make(chan error, 1)

	for iter := uint(0); iter < attempts || attempts == 0; iter++ {
		go func() {
			ch <- fn()
		}()

		select {
		case err := <-ch:
			if err == nil {
				return nil
			}

			if attempts != 0 && iter == attempts-1 {
				return err
			}

		case <-ctx.Done():
			return ctx.Err()
		}
	}

	return nil
}
