package core

import (
	"cmp"
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"slices"

	validation "github.com/go-ozzo/ozzo-validation/v4"
	"github.com/spf13/cast"
)

// ImportCollectionsByMarshaledJSON is the same as [ImportCollections]
// but accept marshaled json array as import data (usually used for the autogenerated snapshots).
func (app *BaseApp) ImportCollectionsByMarshaledJSON(rawSliceOfMaps []byte, deleteMissing bool) error {
	data := []map[string]any{}

	err := json.Unmarshal(rawSliceOfMaps, &data)
	if err != nil {
		return err
	}

	return app.ImportCollections(data, deleteMissing)
}

// ImportCollections imports the provided collections data in a single transaction.
//
// For existing matching collections, the imported data is unmarshaled on top of the existing model.
//
// NB! If deleteMissing is true, ALL NON-SYSTEM COLLECTIONS AND SCHEMA FIELDS,
// that are not present in the imported configuration, WILL BE DELETED
// (this includes their related records data).
func (app *BaseApp) ImportCollections(toImport []map[string]any, deleteMissing bool) error {
	if len(toImport) == 0 {
		// prevent accidentally deleting all collections
		return errors.New("no collections to import")
	}

	importedCollections := make([]*Collection, len(toImport))
	mappedImported := make(map[string]*Collection, len(toImport))

	// normalize imported collections data to ensure that all
	// collection fields are present and properly initialized
	for i, data := range toImport {
		var imported *Collection

		identifier := cast.ToString(data["id"])
		if identifier == "" {
			identifier = cast.ToString(data["name"])
		}

		existing, err := app.FindCollectionByNameOrId(identifier)
		if err != nil && !errors.Is(err, sql.ErrNoRows) {
			return err
		}

		if existing != nil {
			// refetch for deep copy
			imported, err = app.FindCollectionByNameOrId(existing.Id)
			if err != nil {
				return err
			}

			// ensure that the fields will be cleared
			if data["fields"] == nil && deleteMissing {
				data["fields"] = []map[string]any{}
			}

			rawData, err := json.Marshal(data)
			if err != nil {
				return err
			}

			// load the imported data
			err = json.Unmarshal(rawData, imported)
			if err != nil {
				return err
			}

			// extend with the existing fields if necessary
			for _, f := range existing.Fields {
				if !f.GetSystem() && deleteMissing {
					continue
				}
				if imported.Fields.GetById(f.GetId()) == nil {
					// replace with the existing id to prevent accidental column deletion
					// since otherwise the imported field will be treated as a new one
					found := imported.Fields.GetByName(f.GetName())
					if found != nil && found.Type() == f.Type() {
						found.SetId(f.GetId())
					}
					imported.Fields.Add(f)
				}
			}
		} else {
			imported = &Collection{}

			rawData, err := json.Marshal(data)
			if err != nil {
				return err
			}

			// load the imported data
			err = json.Unmarshal(rawData, imported)
			if err != nil {
				return err
			}
		}

		imported.IntegrityChecks(false)

		importedCollections[i] = imported
		mappedImported[imported.Id] = imported
	}

	// reorder views last since the view query could depend on some of the other collections
	slices.SortStableFunc(importedCollections, func(a, b *Collection) int {
		cmpA := -1
		if a.IsView() {
			cmpA = 1
		}

		cmpB := -1
		if b.IsView() {
			cmpB = 1
		}

		res := cmp.Compare(cmpA, cmpB)
		if res == 0 {
			res = a.Created.Compare(b.Created)
			if res == 0 {
				res = a.Updated.Compare(b.Updated)
			}
		}
		return res
	})

	return app.RunInTransaction(func(txApp App) error {
		existingCollections := []*Collection{}
		if err := txApp.CollectionQuery().OrderBy("updated ASC").All(&existingCollections); err != nil {
			return err
		}
		mappedExisting := make(map[string]*Collection, len(existingCollections))
		for _, existing := range existingCollections {
			existing.IntegrityChecks(false)
			mappedExisting[existing.Id] = existing
		}

		// delete old collections not available in the new configuration
		// (before saving the imports in case a deleted collection name is being reused)
		if deleteMissing {
			for _, existing := range existingCollections {
				if mappedImported[existing.Id] != nil || existing.System {
					continue // exist or system
				}

				// delete collection
				if err := txApp.Delete(existing); err != nil {
					return err
				}
			}
		}

		// upsert imported collections
		for _, imported := range importedCollections {
			if err := txApp.SaveNoValidate(imported); err != nil {
				return fmt.Errorf("failed to save collection %q: %w", imported.Name, err)
			}
		}

		// run validations
		for _, imported := range importedCollections {
			original := mappedExisting[imported.Id]
			if original == nil {
				original = imported
			}

			validator := newCollectionValidator(
				context.Background(),
				txApp,
				imported,
				original,
			)
			if err := validator.run(); err != nil {
				// serialize the validation error(s)
				serializedErr, _ := json.MarshalIndent(err, "", "  ")

				return validation.Errors{"collections": validation.NewError(
					"validation_collections_import_failure",
					fmt.Sprintf("Data validations failed for collection %q (%s):\n%s", imported.Name, imported.Id, serializedErr),
				)}
			}
		}

		return nil
	})
}
