//
//  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 (
	"context"
	"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(ctx context.Context, db *gorm.DB, v *entity.UserCanvasVersion) error {
	return db.WithContext(ctx).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(ctx context.Context, db *gorm.DB, id string) (*entity.UserCanvasVersion, error) {
	var v entity.UserCanvasVersion
	err := db.WithContext(ctx).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(ctx context.Context, db *gorm.DB, canvasID string) ([]*entity.UserCanvasVersion, error) {
	var vs []*entity.UserCanvasVersion
	err := db.WithContext(ctx).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(ctx context.Context, db *gorm.DB, canvasID string) (*entity.UserCanvasVersion, error) {
	var v entity.UserCanvasVersion
	err := db.WithContext(ctx).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
}

// GetLatestReleased returns the most recently updated released version of
// canvasID, or ErrUserCanvasVersionNotFound when nothing was ever published.
// Mirrors the released-version lookup in Python's get_agent handler.
func (dao *UserCanvasVersionDAO) GetLatestReleased(canvasID string) (*entity.UserCanvasVersion, error) {
	var v entity.UserCanvasVersion
	err := DB.Where("user_canvas_id = ? AND `release` = ?", canvasID, true).
		Order("update_time DESC").
		First(&v).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrUserCanvasVersionNotFound
		}
		return nil, err
	}
	return &v, nil
}

// GetLatestReleaseTimes returns MAX(create_time) of released versions per
// canvas for the given canvas IDs. Mirrors the release_time aggregation in
// Python's UserCanvasService.get_list.
func (dao *UserCanvasVersionDAO) GetLatestReleaseTimes(canvasIDs []string) (map[string]int64, error) {
	result := make(map[string]int64, len(canvasIDs))
	if len(canvasIDs) == 0 {
		return result, nil
	}
	type row struct {
		UserCanvasID string
		ReleaseTime  int64
	}
	var rows []row
	err := DB.Model(&entity.UserCanvasVersion{}).
		Select("user_canvas_id, MAX(create_time) AS release_time").
		Where("user_canvas_id IN ? AND `release` = ?", canvasIDs, true).
		Group("user_canvas_id").
		Scan(&rows).Error
	if err != nil {
		return nil, err
	}
	for _, r := range rows {
		result[r.UserCanvasID] = r.ReleaseTime
	}
	return result, nil
}

// Delete removes a single version by id. No-op when the row is absent.
func (dao *UserCanvasVersionDAO) Delete(ctx context.Context, db *gorm.DB, id string) error {
	return db.WithContext(ctx).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(ctx context.Context, tx *gorm.DB, id string) error {
	return tx.WithContext(ctx).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(ctx context.Context, db *gorm.DB, canvasID string) (int64, error) {
	res := db.WithContext(ctx).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(ctx context.Context, tx *gorm.DB, canvasID string) (int64, error) {
	res := tx.WithContext(ctx).Where("user_canvas_id = ?", canvasID).Delete(&entity.UserCanvasVersion{})
	return res.RowsAffected, res.Error
}

// CreateTx is the transactional variant of Create.
func (dao *UserCanvasVersionDAO) CreateTx(ctx context.Context, tx *gorm.DB, v *entity.UserCanvasVersion) error {
	return tx.WithContext(ctx).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(ctx context.Context, db *gorm.DB, opts SaveOrReplaceLatestVersionOptions) (*entity.UserCanvasVersion, error) {
	if opts.KeepUnpublished <= 0 {
		opts.KeepUnpublished = 20
	}
	var saved *entity.UserCanvasVersion
	if err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		row, err := dao.SaveOrReplaceLatestTx(ctx, 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(ctx context.Context, tx *gorm.DB, opts SaveOrReplaceLatestVersionOptions) (*entity.UserCanvasVersion, error) {
	if opts.KeepUnpublished <= 0 {
		opts.KeepUnpublished = 20
	}
	var parent struct {
		ID string
	}
	if err := tx.WithContext(ctx).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.WithContext(ctx).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.WithContext(ctx).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(ctx, 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.WithContext(ctx).Create(row).Error; err != nil {
		return nil, err
	}
	return row, dao.deleteAllUnpublishedExcessTx(ctx, 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)
}

func (dao *UserCanvasVersionDAO) deleteAllUnpublishedExcessTx(ctx context.Context, tx *gorm.DB, canvasID string, keep int) error {
	if keep < 0 {
		keep = 0
	}
	var ids []string
	if err := tx.WithContext(ctx).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.WithContext(ctx).Where("id IN ?", ids).Delete(&entity.UserCanvasVersion{}).Error
}
