package opensearch

import (
	"bytes"
	"context"
	"embed"
	"errors"
	"fmt"
	"path"
	"reflect"

	"github.com/go-jose/go-jose/v3/json"
	opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
	"github.com/tidwall/gjson"
)

var (
	ErrManualActionRequired                  = errors.New("manual action required")
	IndexManagerLatest                       = IndexIndexManagerResourceV2
	IndexIndexManagerResourceV1 IndexManager = "resource_v1.json"
	IndexIndexManagerResourceV2 IndexManager = "resource_v2.json"
)

//go:embed internal/indexes/*.json
var indexes embed.FS

type IndexManager string

func (m IndexManager) String() string {
	b, err := m.MarshalJSON()
	if err != nil {
		return ""
	}

	return string(b)
}

func (m IndexManager) MarshalJSON() ([]byte, error) {
	filePath := string(m)
	body, err := indexes.ReadFile(path.Join("./internal/indexes", filePath))
	switch {
	case err != nil:
		return nil, fmt.Errorf("failed to read index file %s: %w", filePath, err)
	case len(body) <= 0:
		return nil, fmt.Errorf("index file %s is empty", filePath)
	}

	return body, nil
}

func (m IndexManager) Apply(ctx context.Context, name string, client *opensearchgoAPI.Client) error {
	localIndexB, err := m.MarshalJSON()
	if err != nil {
		return fmt.Errorf("failed to marshal index %s: %w", name, err)
	}

	indicesExistsResp, err := client.Indices.Exists(ctx, opensearchgoAPI.IndicesExistsReq{
		Indices: []string{name},
	})
	switch {
	case indicesExistsResp != nil && indicesExistsResp.StatusCode == 404:
		break
	case err != nil:
		return fmt.Errorf("failed to check if index %s exists: %w", name, err)
	case indicesExistsResp == nil:
		return fmt.Errorf("indicesExistsResp is nil for index %s", name)
	}

	if indicesExistsResp.StatusCode == 200 {
		resp, err := client.Indices.Get(ctx, opensearchgoAPI.IndicesGetReq{
			Indices: []string{name},
		})
		if err != nil {
			return fmt.Errorf("failed to get index %s: %w", name, err)
		}

		remoteIndex, ok := resp.Indices[name]
		if !ok {
			return fmt.Errorf("index %s not found in response", name)
		}
		remoteIndexB, err := json.Marshal(remoteIndex)
		if err != nil {
			return fmt.Errorf("failed to marshal index %s: %w", name, err)
		}

		localIndexJson := gjson.ParseBytes(localIndexB)
		remoteIndexJson := gjson.ParseBytes(remoteIndexB)

		compare := func(lvPath, rvPath string) (any, any, bool) {
			lv := localIndexJson.Get(lvPath).Raw
			rv := remoteIndexJson.Get(rvPath).Raw

			var lvv, rvv any
			if err := json.Unmarshal([]byte(lv), &lvv); err != nil {
				return nil, nil, false
			}

			if err := json.Unmarshal([]byte(rv), &rvv); err != nil {
				return nil, nil, false
			}

			return lv, rv, reflect.DeepEqual(lvv, rvv)
		}

		var errs []error

		for k := range localIndexJson.Get("settings").Map() {
			if lv, rv, ok := compare("settings."+k, "settings.index."+k); !ok {
				errs = append(errs, fmt.Errorf("settings.%s local %s, remote %s", k, lv, rv))
			}
		}

		for k := range localIndexJson.Get("mappings.properties").Map() {
			if _, _, ok := compare("mappings.properties."+k, "mappings.properties."+k); !ok {
				errs = append(errs, fmt.Errorf("mappings.properties.%s", k))
			}
		}

		if errs != nil {
			return fmt.Errorf(
				"index %s already exists and is different from the requested version, %w: %w",
				name,
				ErrManualActionRequired,
				errors.Join(errs...),
			)
		}

		return nil // Index is already up to date, no action needed
	}

	createResp, err := client.Indices.Create(ctx, opensearchgoAPI.IndicesCreateReq{
		Index: name,
		Body:  bytes.NewReader(localIndexB),
	})
	switch {
	case err != nil:
		return fmt.Errorf("failed to create index %s: %w", name, err)
	case !createResp.Acknowledged:
		return fmt.Errorf("failed to create index %s: not acknowledged", name)
	}

	return nil
}
