package daos

import (
	"errors"
	"fmt"
	"strings"

	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase/models"
	"github.com/pocketbase/pocketbase/models/schema"
	"github.com/pocketbase/pocketbase/tools/list"
	"github.com/pocketbase/pocketbase/tools/types"
)

// RecordQuery returns a new Record select query.
func (dao *Dao) RecordQuery(collection *models.Collection) *dbx.SelectQuery {
	tableName := collection.Name
	selectCols := fmt.Sprintf("%s.*", dao.DB().QuoteSimpleColumnName(tableName))

	return dao.DB().Select(selectCols).From(tableName)
}

// FindRecordById finds the Record model by its id.
func (dao *Dao) FindRecordById(
	collection *models.Collection,
	recordId string,
	filter func(q *dbx.SelectQuery) error,
) (*models.Record, error) {
	tableName := collection.Name

	query := dao.RecordQuery(collection).
		AndWhere(dbx.HashExp{tableName + ".id": recordId})

	if filter != nil {
		if err := filter(query); err != nil {
			return nil, err
		}
	}

	row := dbx.NullStringMap{}
	if err := query.Limit(1).One(row); err != nil {
		return nil, err
	}

	return models.NewRecordFromNullStringMap(collection, row), nil
}

// FindRecordsByIds finds all Record models by the provided ids.
// If no records are found, returns an empty slice.
func (dao *Dao) FindRecordsByIds(
	collection *models.Collection,
	recordIds []string,
	filter func(q *dbx.SelectQuery) error,
) ([]*models.Record, error) {
	tableName := collection.Name

	query := dao.RecordQuery(collection).
		AndWhere(dbx.In(tableName+".id", list.ToInterfaceSlice(recordIds)...))

	if filter != nil {
		if err := filter(query); err != nil {
			return nil, err
		}
	}

	rows := []dbx.NullStringMap{}
	if err := query.All(&rows); err != nil {
		return nil, err
	}

	return models.NewRecordsFromNullStringMaps(collection, rows), nil
}

// FindRecordsByExpr finds all records by the provided db expression.
// If no records are found, returns an empty slice.
//
// Example:
//	expr := dbx.HashExp{"email": "test@example.com"}
//	dao.FindRecordsByExpr(collection, expr)
func (dao *Dao) FindRecordsByExpr(collection *models.Collection, expr dbx.Expression) ([]*models.Record, error) {
	if expr == nil {
		return nil, errors.New("Missing filter expression")
	}

	rows := []dbx.NullStringMap{}

	err := dao.RecordQuery(collection).
		AndWhere(expr).
		All(&rows)

	if err != nil {
		return nil, err
	}

	return models.NewRecordsFromNullStringMaps(collection, rows), nil
}

// FindFirstRecordByData returns the first found record matching
// the provided key-value pair.
func (dao *Dao) FindFirstRecordByData(collection *models.Collection, key string, value any) (*models.Record, error) {
	row := dbx.NullStringMap{}

	err := dao.RecordQuery(collection).
		AndWhere(dbx.HashExp{key: value}).
		Limit(1).
		One(row)

	if err != nil {
		return nil, err
	}

	return models.NewRecordFromNullStringMap(collection, row), nil
}

// IsRecordValueUnique checks if the provided key-value pair is a unique Record value.
//
// NB! Array values (eg. from multiple select fields) are matched
// as a serialized json strings (eg. `["a","b"]`), so the value uniqueness
// depends on the elements order. Or in other words the following values
// are considered different: `[]string{"a","b"}` and `[]string{"b","a"}`
func (dao *Dao) IsRecordValueUnique(
	collection *models.Collection,
	key string,
	value any,
	excludeId string,
) bool {
	var exists bool

	var normalizedVal any
	switch val := value.(type) {
	case []string:
		normalizedVal = append(types.JsonArray{}, list.ToInterfaceSlice(val)...)
	case []any:
		normalizedVal = append(types.JsonArray{}, val...)
	default:
		normalizedVal = val
	}

	err := dao.RecordQuery(collection).
		Select("count(*)").
		AndWhere(dbx.Not(dbx.HashExp{"id": excludeId})).
		AndWhere(dbx.HashExp{key: normalizedVal}).
		Limit(1).
		Row(&exists)

	return err == nil && !exists
}

// FindUserRelatedRecords returns all records that has a reference
// to the provided User model (via the user shema field).
func (dao *Dao) FindUserRelatedRecords(user *models.User) ([]*models.Record, error) {
	collections, err := dao.FindCollectionsWithUserFields()
	if err != nil {
		return nil, err
	}

	result := []*models.Record{}
	for _, collection := range collections {
		userFields := []*schema.SchemaField{}

		// prepare fields options
		if err := collection.Schema.InitFieldsOptions(); err != nil {
			return nil, err
		}

		// extract user fields
		for _, field := range collection.Schema.Fields() {
			if field.Type == schema.FieldTypeUser {
				userFields = append(userFields, field)
			}
		}

		// fetch records associated to the user
		exprs := []dbx.Expression{}
		for _, field := range userFields {
			exprs = append(exprs, dbx.HashExp{field.Name: user.Id})
		}
		rows := []dbx.NullStringMap{}
		if err := dao.RecordQuery(collection).AndWhere(dbx.Or(exprs...)).All(&rows); err != nil {
			return nil, err
		}
		records := models.NewRecordsFromNullStringMaps(collection, rows)

		result = append(result, records...)
	}

	return result, nil
}

// SaveRecord upserts the provided Record model.
func (dao *Dao) SaveRecord(record *models.Record) error {
	return dao.Save(record)
}

// DeleteRecord deletes the provided Record model.
//
// This method will also cascade the delete operation to all linked
// relational records (delete or set to NULL, depending on the rel settings).
//
// The delete operation may fail if the record is part of a required
// reference in another record (aka. cannot be deleted or set to NULL).
func (dao *Dao) DeleteRecord(record *models.Record) error {
	// check for references
	// note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction
	refs, err := dao.FindCollectionReferences(record.Collection(), "")
	if err != nil {
		return err
	}

	// check if related records has to be deleted (if `CascadeDelete` is set)
	// OR
	// just unset the record id from any relation field values (if they are not required)
	// -----------------------------------------------------------
	return dao.RunInTransaction(func(txDao *Dao) error {
		for refCollection, fields := range refs {
			for _, field := range fields {
				options, _ := field.Options.(*schema.RelationOptions)

				rows := []dbx.NullStringMap{}

				// note: the select is not using the transaction dao to prevent SQLITE_LOCKED error when mixing read&write in a single transaction
				err := dao.RecordQuery(refCollection).
					AndWhere(dbx.Not(dbx.HashExp{"id": record.Id})).
					AndWhere(dbx.Like(field.Name, record.Id).Match(true, true)).
					All(&rows)
				if err != nil {
					return err
				}

				refRecords := models.NewRecordsFromNullStringMaps(refCollection, rows)
				for _, refRecord := range refRecords {
					ids := refRecord.GetStringSliceDataValue(field.Name)

					// unset the record id
					for i := len(ids) - 1; i >= 0; i-- {
						if ids[i] == record.Id {
							ids = append(ids[:i], ids[i+1:]...)
							break
						}
					}

					// cascade delete the reference
					// (only if there are no other active references in case of multiple select)
					if options.CascadeDelete && len(ids) == 0 {
						if err := txDao.DeleteRecord(refRecord); err != nil {
							return err
						}
						// no further action are needed (the reference is deleted)
						continue
					}

					if field.Required && len(ids) == 0 {
						return fmt.Errorf("The record cannot be deleted because it is part of a required reference in record %s (%s collection).", refRecord.Id, refCollection.Name)
					}

					// save the reference changes
					refRecord.SetDataValue(field.Name, field.PrepareValue(ids))
					if err := txDao.SaveRecord(refRecord); err != nil {
						return err
					}
				}
			}
		}

		return txDao.Delete(record)
	})
}

// SyncRecordTableSchema compares the two provided collections
// and applies the necessary related record table changes.
//
// If `oldCollection` is null, then only `newCollection` is used to create the record table.
func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldCollection *models.Collection) error {
	// create
	if oldCollection == nil {
		cols := map[string]string{
			schema.ReservedFieldNameId:      "TEXT PRIMARY KEY",
			schema.ReservedFieldNameCreated: `TEXT DEFAULT "" NOT NULL`,
			schema.ReservedFieldNameUpdated: `TEXT DEFAULT "" NOT NULL`,
		}

		tableName := newCollection.Name

		// add schema field definitions
		for _, field := range newCollection.Schema.Fields() {
			cols[field.Name] = field.ColDefinition()
		}

		// create table
		_, tableErr := dao.DB().CreateTable(tableName, cols).Execute()
		if tableErr != nil {
			return tableErr
		}

		// add index on the base `created` column
		_, indexErr := dao.DB().CreateIndex(tableName, tableName+"_created_idx", "created").Execute()
		if indexErr != nil {
			return indexErr
		}

		return nil
	}

	// update
	return dao.RunInTransaction(func(txDao *Dao) error {
		oldTableName := oldCollection.Name
		newTableName := newCollection.Name
		oldSchema := oldCollection.Schema
		newSchema := newCollection.Schema

		// check for renamed table
		if !strings.EqualFold(oldTableName, newTableName) {
			_, err := dao.DB().RenameTable(oldTableName, newTableName).Execute()
			if err != nil {
				return err
			}
		}

		// check for deleted columns
		for _, oldField := range oldSchema.Fields() {
			if f := newSchema.GetFieldById(oldField.Id); f != nil {
				continue // exist
			}

			_, err := txDao.DB().DropColumn(newTableName, oldField.Name).Execute()
			if err != nil {
				return err
			}
		}

		// check for new or renamed columns
		for _, field := range newSchema.Fields() {
			oldField := oldSchema.GetFieldById(field.Id)
			if oldField != nil {
				// rename
				_, err := txDao.DB().RenameColumn(newTableName, oldField.Name, field.Name).Execute()
				if err != nil {
					return err
				}
			} else {
				// add
				_, err := txDao.DB().AddColumn(newTableName, field.Name, field.ColDefinition()).Execute()
				if err != nil {
					return err
				}
			}
		}

		return nil
	})
}
