package kb

import (
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/kbgovernance"

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

const (
	PublicationRequestStatusPendingApproval = "pending_approval"
	PublicationRequestStatusRejected        = "rejected"
	PublicationRequestStatusWithdrawn       = "withdrawn"
	PublicationRequestStatusStale           = "stale"
	PublicationRequestStatusPublished       = "published"
)

var (
	ErrPublicationAlreadyPending = errors.New("publication request already pending")
	ErrPublicationReplayConflict = errors.New("publication request replay conflict")
	ErrPublicationSnapshotStale  = errors.New("publication snapshot is stale")
)

// KnowledgeBasePublicationRequest is the durable MeshX side of one generic
// kb_publication Approval. SnapshotJSON is the immutable reviewed revision;
// the live state must still match ContentDigest when approval is applied.
type KnowledgeBasePublicationRequest struct {
	ID                  string     `gorm:"primaryKey;size:64"`
	WorkspaceID         string     `gorm:"size:64;not null;index:idx_kb_publication_requests_kb,priority:1;uniqueIndex:uk_kb_publication_request_replay,priority:1"`
	KnowledgeBaseID     string     `gorm:"size:64;not null;index:idx_kb_publication_requests_kb,priority:2"`
	WorkingRevision     string     `gorm:"size:64;not null"`
	RequesterID         string     `gorm:"size:64;not null;uniqueIndex:uk_kb_publication_request_replay,priority:2"`
	ApprovalID          string     `gorm:"size:64;not null;uniqueIndex"`
	ApprovalVersion     int64      `gorm:"not null"`
	Status              string     `gorm:"size:32;not null;index:idx_kb_publication_requests_kb,priority:3"`
	IdempotencyKey      string     `gorm:"size:128;not null;uniqueIndex:uk_kb_publication_request_replay,priority:3"`
	PayloadDigest       string     `gorm:"size:64;not null"`
	ContentDigest       string     `gorm:"size:64;not null"`
	SnapshotJSON        string     `gorm:"type:text;not null"`
	SourceCount         int        `gorm:"not null;default:0"`
	WikiReadyCount      int        `gorm:"not null;default:0"`
	DashboardReadyCount int        `gorm:"not null;default:0"`
	SkippedCount        int        `gorm:"not null;default:0"`
	Decision            string     `gorm:"size:32;not null;default:''"`
	DecisionID          string     `gorm:"size:128;not null;default:''"`
	DecisionInputReason string     `gorm:"type:text;not null"`
	DecisionReason      string     `gorm:"type:text;not null"`
	DecidedBy           string     `gorm:"size:64;not null;default:''"`
	Version             int64      `gorm:"not null;default:1"`
	CreatedAt           time.Time  `gorm:"not null"`
	UpdatedAt           time.Time  `gorm:"not null;index:idx_kb_publication_requests_kb,priority:4"`
	DecidedAt           *time.Time `gorm:"index"`
	PublishedAt         *time.Time `gorm:"index"`
}

func (KnowledgeBasePublicationRequest) TableName() string {
	return "knowledge_base_publication_requests"
}

type PublicationSnapshotResult struct {
	Snapshot            kbgovernance.PublicationSnapshot
	JSON                []byte
	Digest              string
	SourceCount         int
	WikiReadyCount      int
	DashboardReadyCount int
	SkippedCount        int
}

// LoadKnowledgeBasePublicationSnapshot returns one canonical, bounded review
// snapshot. Callers inside a transaction may request row locks so approval and
// publication are fenced against concurrent edits.
func (s *Store) LoadKnowledgeBasePublicationSnapshot(ctx context.Context, workspaceID, kbID string, lock bool) (PublicationSnapshotResult, error) {
	if err := s.ensureDB(); err != nil {
		return PublicationSnapshotResult{}, err
	}
	query := func() *gorm.DB {
		result := s.db.WithContext(ctx)
		if lock {
			result = result.Clauses(clause.Locking{Strength: "UPDATE"})
		}
		return result
	}
	var base KnowledgeBase
	if err := query().Where("workspace_id = ? AND id = ? AND deleted_at IS NULL", strings.TrimSpace(workspaceID), strings.TrimSpace(kbID)).First(&base).Error; err != nil {
		return PublicationSnapshotResult{}, err
	}
	var session KnowledgeBaseSetupSession
	if err := query().Where("workspace_id = ? AND knowledge_base_id = ?", base.WorkspaceID, base.ID).First(&session).Error; err != nil {
		return PublicationSnapshotResult{}, err
	}
	return s.loadKnowledgeBasePublicationSnapshot(ctx, base, session.WorkingRevision, lock)
}

func (s *Store) CreateKnowledgeBasePublicationRequest(ctx context.Context, request KnowledgeBasePublicationRequest, now time.Time) (KnowledgeBasePublicationRequest, bool, error) {
	if err := s.ensureDB(); err != nil {
		return KnowledgeBasePublicationRequest{}, false, err
	}
	if err := validatePublicationRequest(request, now); err != nil {
		return KnowledgeBasePublicationRequest{}, false, err
	}
	var stored KnowledgeBasePublicationRequest
	created := false
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		store := s.WithDB(tx)
		var replay KnowledgeBasePublicationRequest
		err := tx.Where("workspace_id = ? AND requester_id = ? AND idempotency_key = ?", request.WorkspaceID, request.RequesterID, request.IdempotencyKey).First(&replay).Error
		if err == nil {
			if !samePublicationRequest(replay, request) {
				return ErrPublicationReplayConflict
			}
			stored = replay
			return nil
		}
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}
		snapshot, err := store.LoadKnowledgeBasePublicationSnapshot(ctx, request.WorkspaceID, request.KnowledgeBaseID, true)
		if err != nil {
			return err
		}
		if snapshot.Snapshot.WorkingRevision != request.WorkingRevision || snapshot.Digest != request.ContentDigest || string(snapshot.JSON) != request.SnapshotJSON {
			return ErrPublicationSnapshotStale
		}
		var base KnowledgeBase
		if err := tx.Where("workspace_id = ? AND id = ? AND publication_state = ?", request.WorkspaceID, request.KnowledgeBaseID, "draft").First(&base).Error; err != nil {
			return err
		}
		var active KnowledgeBasePublicationRequest
		err = tx.Where("workspace_id = ? AND knowledge_base_id = ? AND status = ?", request.WorkspaceID, request.KnowledgeBaseID, PublicationRequestStatusPendingApproval).First(&active).Error
		if err == nil {
			return ErrPublicationAlreadyPending
		}
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}
		if err := tx.Create(&request).Error; err != nil {
			return fmt.Errorf("create publication request: %w", err)
		}
		stored, created = request, true
		return nil
	})
	return stored, created, err
}

func validatePublicationRequest(request KnowledgeBasePublicationRequest, now time.Time) error {
	if request.ID == "" || request.WorkspaceID == "" || request.KnowledgeBaseID == "" || request.WorkingRevision == "" || request.RequesterID == "" || request.ApprovalID == "" || request.ApprovalVersion <= 0 || request.IdempotencyKey == "" || now.IsZero() {
		return fmt.Errorf("create publication request: scoped identities are required")
	}
	if request.Status != PublicationRequestStatusPendingApproval || len(request.PayloadDigest) != 64 || len(request.ContentDigest) != 64 || request.SnapshotJSON == "" {
		return fmt.Errorf("create publication request: frozen payload is invalid")
	}
	if _, err := hex.DecodeString(request.PayloadDigest); err != nil {
		return fmt.Errorf("create publication request: payload digest is invalid")
	}
	if _, err := hex.DecodeString(request.ContentDigest); err != nil {
		return fmt.Errorf("create publication request: content digest is invalid")
	}
	return nil
}

func samePublicationRequest(left, right KnowledgeBasePublicationRequest) bool {
	return left.WorkspaceID == right.WorkspaceID && left.KnowledgeBaseID == right.KnowledgeBaseID &&
		left.WorkingRevision == right.WorkingRevision && left.RequesterID == right.RequesterID &&
		left.ApprovalID == right.ApprovalID && left.ApprovalVersion == right.ApprovalVersion &&
		left.PayloadDigest == right.PayloadDigest && left.ContentDigest == right.ContentDigest && left.SnapshotJSON == right.SnapshotJSON
}

func (s *Store) GetLatestKnowledgeBasePublicationRequest(ctx context.Context, workspaceID, kbID string) (KnowledgeBasePublicationRequest, error) {
	if err := s.ensureDB(); err != nil {
		return KnowledgeBasePublicationRequest{}, err
	}
	var request KnowledgeBasePublicationRequest
	err := s.db.WithContext(ctx).Where("workspace_id = ? AND knowledge_base_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(kbID)).Order("created_at DESC, id DESC").First(&request).Error
	return request, err
}

func (s *Store) GetKnowledgeBasePublicationRequestByApproval(ctx context.Context, workspaceID, approvalID string) (KnowledgeBasePublicationRequest, error) {
	if err := s.ensureDB(); err != nil {
		return KnowledgeBasePublicationRequest{}, err
	}
	var request KnowledgeBasePublicationRequest
	err := s.db.WithContext(ctx).Where("workspace_id = ? AND approval_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(approvalID)).First(&request).Error
	return request, err
}

type ResolveKnowledgeBasePublicationParams struct {
	WorkspaceID        string
	ApprovalID         string
	ApprovalVersion    int64
	ActorID            string
	ClientDecisionID   string
	Decision           string
	Reason             string
	AuthorizationValid bool
	AuditEvent         AuditEvent
	Now                time.Time
}

// ResolveKnowledgeBasePublication applies one generic Approval receipt. The
// approved path verifies and publishes the complete frozen snapshot in the
// same transaction; drift produces a terminal stale request instead.
func (s *Store) ResolveKnowledgeBasePublication(ctx context.Context, params ResolveKnowledgeBasePublicationParams) (KnowledgeBasePublicationRequest, bool, error) {
	if err := s.ensureDB(); err != nil {
		return KnowledgeBasePublicationRequest{}, false, err
	}
	if params.WorkspaceID == "" || params.ApprovalID == "" || params.ApprovalVersion <= 0 || params.ActorID == "" || params.ClientDecisionID == "" || params.Now.IsZero() {
		return KnowledgeBasePublicationRequest{}, false, fmt.Errorf("resolve publication Approval: identities are required")
	}
	if params.Decision != GenerationDecisionApproved && params.Decision != GenerationDecisionRejected && params.Decision != GenerationDecisionWithdrawn {
		return KnowledgeBasePublicationRequest{}, false, fmt.Errorf("resolve publication Approval: decision is invalid")
	}
	if params.Decision == GenerationDecisionRejected && strings.TrimSpace(params.Reason) == "" {
		return KnowledgeBasePublicationRequest{}, false, fmt.Errorf("resolve publication Approval: rejection reason is required")
	}
	var resolved KnowledgeBasePublicationRequest
	applied := false
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var request KnowledgeBasePublicationRequest
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("workspace_id = ? AND approval_id = ?", params.WorkspaceID, params.ApprovalID).First(&request).Error; err != nil {
			return err
		}
		if request.DecisionID != "" {
			if request.DecisionID != params.ClientDecisionID || request.Decision != params.Decision || request.DecidedBy != params.ActorID || request.DecisionInputReason != strings.TrimSpace(params.Reason) || request.ApprovalVersion != params.ApprovalVersion {
				return ErrPublicationReplayConflict
			}
			resolved = request
			return nil
		}
		if request.Status != PublicationRequestStatusPendingApproval || params.ApprovalVersion != request.ApprovalVersion+1 {
			return ErrPublicationReplayConflict
		}
		status, reason := PublicationRequestStatusRejected, strings.TrimSpace(params.Reason)
		var publishedAt *time.Time
		if params.Decision == GenerationDecisionWithdrawn {
			status = PublicationRequestStatusWithdrawn
		} else if params.Decision == GenerationDecisionApproved {
			if !params.AuthorizationValid {
				status, reason = PublicationRequestStatusStale, "authorization_changed"
			} else {
				snapshot, err := s.WithDB(tx).LoadKnowledgeBasePublicationSnapshot(ctx, request.WorkspaceID, request.KnowledgeBaseID, true)
				if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
					return err
				}
				if err != nil || snapshot.Snapshot.WorkingRevision != request.WorkingRevision || snapshot.Digest != request.ContentDigest || string(snapshot.JSON) != request.SnapshotJSON {
					status, reason = PublicationRequestStatusStale, "working_revision_changed"
				} else {
					baseUpdate := tx.Model(&KnowledgeBase{}).Where("workspace_id = ? AND id = ? AND publication_state = ? AND working_revision = ?", request.WorkspaceID, request.KnowledgeBaseID, "draft", request.WorkingRevision).Updates(map[string]any{
						"publication_state": "official", "published_revision": request.WorkingRevision,
						"working_revision": "",
						"published_at":     params.Now, "updated_by": params.ActorID, "updated_at": params.Now,
					})
					if baseUpdate.Error != nil {
						return baseUpdate.Error
					}
					if baseUpdate.RowsAffected != 1 {
						status, reason = PublicationRequestStatusStale, "working_revision_changed"
					} else {
						sessionUpdate := tx.Model(&KnowledgeBaseSetupSession{}).Where("workspace_id = ? AND knowledge_base_id = ? AND working_revision = ?", request.WorkspaceID, request.KnowledgeBaseID, request.WorkingRevision).Updates(map[string]any{
							"published_revision":    request.WorkingRevision,
							"working_snapshot_json": request.SnapshotJSON, "published_snapshot_json": request.SnapshotJSON,
							"completed_at": params.Now, "updated_by": params.ActorID,
							"updated_at": params.Now, "version": gorm.Expr("version + 1"),
						})
						if sessionUpdate.Error != nil {
							return sessionUpdate.Error
						}
						if sessionUpdate.RowsAffected != 1 {
							return ErrPublicationSnapshotStale
						}
						status, reason, publishedAt = PublicationRequestStatusPublished, "", &params.Now
					}
				}
			}
		}
		updates := map[string]any{
			"status": status, "approval_version": params.ApprovalVersion,
			"decision": params.Decision, "decision_id": params.ClientDecisionID,
			"decision_input_reason": strings.TrimSpace(params.Reason), "decision_reason": reason,
			"decided_by": params.ActorID, "decided_at": params.Now, "published_at": publishedAt,
			"updated_at": params.Now, "version": gorm.Expr("version + 1"),
		}
		result := tx.Model(&KnowledgeBasePublicationRequest{}).Where("workspace_id = ? AND id = ? AND status = ? AND decision_id = ''", request.WorkspaceID, request.ID, PublicationRequestStatusPendingApproval).Updates(updates)
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected != 1 {
			return ErrPublicationReplayConflict
		}
		if strings.TrimSpace(params.AuditEvent.ID) != "" {
			if _, err := s.WithDB(tx).CreateAuditEvent(ctx, params.AuditEvent); err != nil {
				return err
			}
		}
		if err := tx.Where("workspace_id = ? AND id = ?", request.WorkspaceID, request.ID).First(&resolved).Error; err != nil {
			return err
		}
		applied = true
		return nil
	})
	return resolved, applied, err
}
