//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

package dao

import (
	"errors"
	"reflect"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"ragflow/internal/entity"
)

// ErrUserCanvasVersionNotFound is returned when a version lookup by id or
// canvas id yields no rows. Service and handler layers map this to a 404.
var ErrUserCanvasVersionNotFound = errors.New("user_canvas_version: not found")

// UserCanvasVersionDAO persists and queries UserCanvasVersion rows.
type UserCanvasVersionDAO struct{}

// SaveOrReplaceLatestVersionOptions controls a version-history save.
type SaveOrReplaceLatestVersionOptions struct {
	NewID           string
	UserCanvasID    string
	Title           *string
	Description     *string
	DSL             entity.JSONMap
	Release         bool
	KeepUnpublished int
	SameDSL         func(entity.JSONMap) bool
}

// NewUserCanvasVersionDAO returns a zero-value DAO. The struct is stateless
// so callers can share a single instance or create their own.
func NewUserCanvasVersionDAO() *UserCanvasVersionDAO {
	return &UserCanvasVersionDAO{}
}

// Create inserts a new version row. The caller assigns ID, UserCanvasID,
// Title, Description, DSL. CreateTime/UpdateTime are stamped by the
// BaseModel BeforeCreate hook.
func (dao *UserCanvasVersionDAO) Create(v *entity.UserCanvasVersion) error {
	return DB.Create(v).Error
}

// GetByID fetches a single version by primary key. Returns
// ErrUserCanvasVersionNotFound when the row is absent so callers can map
// to a 404 instead of inspecting gorm.ErrRecordNotFound directly.
func (dao *UserCanvasVersionDAO) GetByID(id string) (*entity.UserCanvasVersion, error) {
	var v entity.UserCanvasVersion
	err := DB.Where("id = ?", id).First(&v).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrUserCanvasVersionNotFound
		}
		return nil, err
	}
	return &v, nil
}

// ListByCanvasID returns every version of the given canvas, ordered by
// create_time DESC so the most recent publish appears first.
func (dao *UserCanvasVersionDAO) ListByCanvasID(canvasID string) ([]*entity.UserCanvasVersion, error) {
	var vs []*entity.UserCanvasVersion
	err := DB.Where("user_canvas_id = ?", canvasID).
		Order("create_time DESC").
		Find(&vs).Error
	return vs, err
}

// GetLatest returns the most recently created version of canvasID, or
// ErrUserCanvasVersionNotFound when the canvas has never been published.
func (dao *UserCanvasVersionDAO) GetLatest(canvasID string) (*entity.UserCanvasVersion, error) {
	var v entity.UserCanvasVersion
	err := DB.Where("user_canvas_id = ?", canvasID).
		Order("create_time DESC").
		First(&v).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrUserCanvasVersionNotFound
		}
		return nil, err
	}
	return &v, nil
}

// Delete removes a single version by id. No-op when the row is absent.
func (dao *UserCanvasVersionDAO) Delete(id string) error {
	return DB.Where("id = ?", id).Delete(&entity.UserCanvasVersion{}).Error
}

// DeleteTx is the transactional variant of Delete. Used by
// service.AgentService.DeleteVersion so the version-row removal and the
// (future) parent-canvas stat update land in one atomic write.
func (dao *UserCanvasVersionDAO) DeleteTx(tx *gorm.DB, id string) error {
	return tx.Where("id = ?", id).Delete(&entity.UserCanvasVersion{}).Error
}

// DeleteByCanvasID removes every version of the given canvas. Called from
// the service layer when the parent canvas is deleted to enforce the
// §2.9 cascade rule. Returns the number of rows actually deleted.
func (dao *UserCanvasVersionDAO) DeleteByCanvasID(canvasID string) (int64, error) {
	res := DB.Where("user_canvas_id = ?", canvasID).Delete(&entity.UserCanvasVersion{})
	return res.RowsAffected, res.Error
}

// DeleteByCanvasIDTx is the transactional variant of DeleteByCanvasID.
// Used by service.AgentService.DeleteAgent so the cascade runs atomically
// with the parent canvas row removal.
func (dao *UserCanvasVersionDAO) DeleteByCanvasIDTx(tx *gorm.DB, canvasID string) (int64, error) {
	res := tx.Where("user_canvas_id = ?", canvasID).Delete(&entity.UserCanvasVersion{})
	return res.RowsAffected, res.Error
}

// CreateTx is the transactional variant of Create.
func (dao *UserCanvasVersionDAO) CreateTx(tx *gorm.DB, v *entity.UserCanvasVersion) error {
	return tx.Create(v).Error
}

// SaveOrReplaceLatest inserts a new version or refreshes the latest matching
// draft in place. If the latest matching version is released and the current
// save is a draft, it creates a new draft to preserve the released snapshot.
func (dao *UserCanvasVersionDAO) SaveOrReplaceLatest(opts SaveOrReplaceLatestVersionOptions) (*entity.UserCanvasVersion, error) {
	if opts.KeepUnpublished <= 0 {
		opts.KeepUnpublished = 20
	}
	var saved *entity.UserCanvasVersion
	if err := DB.Transaction(func(tx *gorm.DB) error {
		row, err := dao.SaveOrReplaceLatestTx(tx, opts)
		if err != nil {
			return err
		}
		saved = row
		return nil
	}); err != nil {
		return nil, err
	}
	return saved, nil
}

// SaveOrReplaceLatestTx inserts or refreshes a version using the caller's
// transaction. Its write semantics match SaveOrReplaceLatest exactly.
func (dao *UserCanvasVersionDAO) SaveOrReplaceLatestTx(tx *gorm.DB, opts SaveOrReplaceLatestVersionOptions) (*entity.UserCanvasVersion, error) {
	if opts.KeepUnpublished <= 0 {
		opts.KeepUnpublished = 20
	}
	var parent struct {
		ID string
	}
	if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
		Table((&entity.UserCanvas{}).TableName()).
		Select("id").
		Where("id = ?", opts.UserCanvasID).
		Take(&parent).Error; err != nil {
		return nil, err
	}

	var latest entity.UserCanvasVersion
	err := tx.Where("user_canvas_id = ?", opts.UserCanvasID).
		Order("create_time DESC, id DESC").
		First(&latest).Error
	if err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
	} else if opts.sameDSL(latest.DSL) {
		if !latest.Release || opts.Release {
			updates := map[string]interface{}{
				"dsl":     opts.DSL,
				"release": opts.Release,
			}
			if opts.Title != nil {
				updates["title"] = opts.Title
			}
			if opts.Description != nil {
				updates["description"] = opts.Description
			}
			if err := tx.Model(&entity.UserCanvasVersion{}).
				Where("id = ?", latest.ID).
				Updates(updates).Error; err != nil {
				return nil, err
			}
			latest.DSL = opts.DSL
			latest.Release = opts.Release
			if opts.Title != nil {
				latest.Title = opts.Title
			}
			if opts.Description != nil {
				latest.Description = opts.Description
			}
			return &latest, dao.deleteAllUnpublishedExcessTx(tx, opts.UserCanvasID, opts.KeepUnpublished)
		}
	}
	row := &entity.UserCanvasVersion{
		ID:           opts.NewID,
		UserCanvasID: opts.UserCanvasID,
		Title:        opts.Title,
		Description:  opts.Description,
		Release:      opts.Release,
		DSL:          opts.DSL,
	}
	if err := tx.Create(row).Error; err != nil {
		return nil, err
	}
	return row, dao.deleteAllUnpublishedExcessTx(tx, opts.UserCanvasID, opts.KeepUnpublished)
}

func (opts SaveOrReplaceLatestVersionOptions) sameDSL(dsl entity.JSONMap) bool {
	if opts.SameDSL != nil {
		return opts.SameDSL(dsl)
	}
	return reflect.DeepEqual(dsl, opts.DSL)
}

// DeleteAllUnpublishedExcess keeps the newest keep unpublished versions for a
// canvas and deletes older unpublished rows. Released versions are never
// removed by this cleanup.
func (dao *UserCanvasVersionDAO) DeleteAllUnpublishedExcess(canvasID string, keep int) error {
	return dao.deleteAllUnpublishedExcessTx(DB, canvasID, keep)
}

func (dao *UserCanvasVersionDAO) deleteAllUnpublishedExcessTx(tx *gorm.DB, canvasID string, keep int) error {
	if keep < 0 {
		keep = 0
	}
	var ids []string
	if err := tx.Model(&entity.UserCanvasVersion{}).
		Where(map[string]interface{}{"user_canvas_id": canvasID, "release": false}).
		Order("create_time DESC").
		Pluck("id", &ids).Error; err != nil {
		return err
	}
	if len(ids) <= keep {
		return nil
	}
	ids = ids[keep:]
	return tx.Where("id IN ?", ids).Delete(&entity.UserCanvasVersion{}).Error
}
