package kb

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

	"context"
	"errors"
	"fmt"
	"reflect"
	"slices"
	"sort"
	"strings"

	storekb "github.com/tidbcloud/AgentTeam/internal/store/kb"
)

type KBOfficialEditBasicMutation struct {
	Name            string
	Description     string
	GroupID         string
	TagIDs          []string
	ExpectedVersion int64
}

type KBOfficialEditSaveMutation struct {
	WorkingRevision   string
	PublishedRevision string
	ContentDigest     string
	ExpectedVersion   int64
	ClientRequestID   string
}

func (s *KBService) ConvertKnowledgeBaseOfficialToDraft(ctx context.Context, workspaceID, subject, kbID string) (KBSetupView, error) {
	base, err := s.GetKnowledgeBase(ctx, workspaceID, subject, kbID)
	if err != nil {
		return KBSetupView{}, err
	}
	if base.PublicationState != KBPublicationOfficial || !base.IsActive {
		return KBSetupView{}, codedConflict("kb-official-to-draft-unavailable", "Only an active Official knowledge base can be converted to Draft", nil)
	}
	if err := s.requireOfficialToDraftConverter(ctx, base, subject); err != nil {
		return KBSetupView{}, err
	}
	now := s.now()
	workingRevision := newKBID("kbr")
	audit := s.auditEvent(base.WorkspaceID, base.ID, subject, "kb.official.converted_to_draft", "knowledge_base", base.ID, "Converted Official knowledge base to Draft", kbJSONString(map[string]string{
		"previous_revision": base.PublishedRevision, "working_revision": workingRevision,
	}))
	_, err = s.store.ConvertKnowledgeBaseOfficialToDraft(ctx, storekb.ConvertKnowledgeBaseOfficialToDraftParams{
		WorkspaceID: base.WorkspaceID, KnowledgeBaseID: base.ID, WorkingRevision: workingRevision,
		ActorID: subject, AuditEvent: audit, Now: now,
	})
	if errors.Is(err, storekb.ErrOfficialToDraftConflict) {
		return KBSetupView{}, codedConflict("kb-official-to-draft-conflict", "The Official knowledge base changed before it could be converted to Draft", err)
	}
	if err != nil {
		return KBSetupView{}, err
	}
	return s.GetKnowledgeBaseSetup(ctx, workspaceID, subject, kbID)
}

func knowledgeBasePublicationMetadataChanges(base storekb.KnowledgeBase, input KBMutation) bool {
	name := base.Name
	if candidate := strings.TrimSpace(input.Name); candidate != "" {
		name = candidate
	}
	if name != base.Name || strings.TrimSpace(input.Description) != base.Description {
		return true
	}
	if input.GroupID != nil && strings.TrimSpace(*input.GroupID) != base.GroupID {
		return true
	}
	if !input.TagIDsSet {
		return false
	}
	current := make([]string, 0, len(base.Tags))
	for _, tag := range base.Tags {
		current = append(current, tag.ID)
	}
	requested := uniqueTaxonomyIDs(input.TagIDs)
	sort.Strings(current)
	sort.Strings(requested)
	return !slices.Equal(current, requested)
}

func (s *KBService) setupRevisionPresentation(ctx context.Context, base storekb.KnowledgeBase, session storekb.KnowledgeBaseSetupSession) (KBSetupBasicInfo, []string, string, error) {
	if session.Mode != KBSetupModeEditOfficial {
		return setupBasicInfoFromBase(base), []string{}, "", nil
	}
	if session.CompletedAt != nil {
		published, err := kbgovernance.DecodePublicationSnapshot([]byte(session.PublishedSnapshotJSON))
		if err != nil || published.KnowledgeBaseID != base.ID || published.WorkingRevision != session.PublishedRevision {
			return KBSetupBasicInfo{}, nil, "", fmt.Errorf("decode completed Official revision snapshot")
		}
		_, digest, err := kbgovernance.EncodePublicationSnapshot(published)
		if err != nil {
			return KBSetupBasicInfo{}, nil, "", err
		}
		return setupBasicInfoFromSnapshot(published), []string{}, digest, nil
	}
	working, err := s.store.LoadKnowledgeBaseOfficialWorkingSnapshot(ctx, session, false)
	if err != nil {
		return KBSetupBasicInfo{}, nil, "", err
	}
	changes, err := officialSnapshotChangeFields(session.PublishedSnapshotJSON, working.Snapshot)
	if err != nil {
		return KBSetupBasicInfo{}, nil, "", err
	}
	return setupBasicInfoFromSnapshot(working.Snapshot), changes, working.Digest, nil
}

func (s *KBService) StartKnowledgeBaseOfficialEdit(ctx context.Context, workspaceID, subject, kbID string) (KBSetupView, error) {
	base, err := s.GetKnowledgeBase(ctx, workspaceID, subject, kbID)
	if err != nil {
		return KBSetupView{}, err
	}
	if base.PublicationState != KBPublicationOfficial || strings.TrimSpace(base.PublishedRevision) == "" || !base.IsActive {
		return KBSetupView{}, codedConflict("kb-official-edit-unavailable", "Only an active Official knowledge base can be edited", nil)
	}
	if err := s.requireOfficialEditor(ctx, base, subject); err != nil {
		return KBSetupView{}, err
	}
	now := s.now()
	workingRevision := newKBID("kbr")
	audit := s.auditEvent(base.WorkspaceID, base.ID, subject, "kb.official_edit.started", "knowledge_base", base.ID, "Started Official knowledge base edit", "")
	_, err = s.store.StartKnowledgeBaseOfficialEdit(ctx, storekb.StartKnowledgeBaseOfficialEditParams{
		WorkspaceID: base.WorkspaceID, KnowledgeBaseID: base.ID, WorkingRevision: workingRevision,
		ActorID: subject, AuditEvent: audit, Now: now,
	})
	if errors.Is(err, storekb.ErrOfficialEditConflict) {
		return KBSetupView{}, codedConflict("kb-official-edit-conflict", "The Official knowledge base changed before editing started", err)
	}
	if err != nil {
		return KBSetupView{}, err
	}
	return s.GetKnowledgeBaseSetup(ctx, workspaceID, subject, kbID)
}

func (s *KBService) UpdateKnowledgeBaseOfficialEditBasic(ctx context.Context, workspaceID, subject, kbID string, input KBOfficialEditBasicMutation) (KBSetupView, error) {
	input.Name, input.Description, input.GroupID = strings.TrimSpace(input.Name), strings.TrimSpace(input.Description), strings.TrimSpace(input.GroupID)
	if input.Name == "" || input.GroupID == "" || input.ExpectedVersion <= 0 {
		return KBSetupView{}, invalidArgument("Official edit name, group, and expected version are required")
	}
	base, err := s.GetKnowledgeBase(ctx, workspaceID, subject, kbID)
	if err != nil {
		return KBSetupView{}, err
	}
	if err := s.requireOfficialEditor(ctx, base, subject); err != nil {
		return KBSetupView{}, err
	}
	groupID, tagIDs, err := s.resolveKnowledgeBaseClassification(ctx, workspaceID, subject, &input.GroupID, input.TagIDs)
	if err != nil {
		return KBSetupView{}, err
	}
	now := s.now()
	audit := s.auditEvent(base.WorkspaceID, base.ID, subject, "kb.official_edit.basic.updated", "knowledge_base", base.ID, "Updated Official working revision basic info", "")
	_, err = s.store.UpdateKnowledgeBaseOfficialEditBasic(ctx, storekb.UpdateKnowledgeBaseOfficialEditBasicParams{
		WorkspaceID: base.WorkspaceID, KnowledgeBaseID: base.ID, ExpectedVersion: input.ExpectedVersion,
		Name: input.Name, Description: input.Description, GroupID: groupID, TagIDs: tagIDs,
		ActorID: subject, AuditEvent: audit, Now: now,
	})
	if errors.Is(err, storekb.ErrOfficialEditConflict) {
		return KBSetupView{}, codedConflict("kb-official-edit-version-conflict", "The Official working revision changed on another device", err)
	}
	if err != nil {
		return KBSetupView{}, err
	}
	return s.GetKnowledgeBaseSetup(ctx, workspaceID, subject, kbID)
}

func (s *KBService) SaveKnowledgeBaseOfficialEdit(ctx context.Context, workspaceID, subject, kbID string, input KBOfficialEditSaveMutation) (storekb.KnowledgeBase, error) {
	input.WorkingRevision, input.PublishedRevision = strings.TrimSpace(input.WorkingRevision), strings.TrimSpace(input.PublishedRevision)
	input.ContentDigest = strings.TrimSpace(input.ContentDigest)
	input.ClientRequestID = strings.TrimSpace(input.ClientRequestID)
	if input.WorkingRevision == "" || input.PublishedRevision == "" || len(input.ContentDigest) != 64 || input.ExpectedVersion <= 0 || input.ClientRequestID == "" || len(input.ClientRequestID) > 128 {
		return storekb.KnowledgeBase{}, invalidArgument("Official save requires exact revisions, content digest, version, and bounded request ID")
	}
	base, err := s.GetKnowledgeBase(ctx, workspaceID, subject, kbID)
	if err != nil {
		return storekb.KnowledgeBase{}, err
	}
	if err := s.requireOfficialEditor(ctx, base, subject); err != nil {
		return storekb.KnowledgeBase{}, err
	}
	setup, err := s.GetKnowledgeBaseSetup(ctx, workspaceID, subject, kbID)
	if err != nil {
		return storekb.KnowledgeBase{}, err
	}
	replay := setup.Session.CompletedAt != nil && setup.Session.SaveRequestID == input.ClientRequestID && setup.Session.PublishedRevision == input.WorkingRevision
	if !replay && (setup.Session.WorkingRevision != input.WorkingRevision || setup.Session.PublishedRevision != input.PublishedRevision || setup.Session.Version != input.ExpectedVersion) {
		return storekb.KnowledgeBase{}, codedConflict("kb-official-edit-version-conflict", "The Official working revision changed; refresh Review", nil)
	}
	if setup.OfficialContentDigest != input.ContentDigest {
		return storekb.KnowledgeBase{}, codedConflict("kb-official-edit-content-conflict", "The Official working content changed; refresh Review", nil)
	}
	if !replay && setup.StepSummary["review"] != "ready" {
		return storekb.KnowledgeBase{}, codedConflict("kb-official-edit-incomplete", "Complete every setup step before saving Official", nil)
	}
	now := s.now()
	audit := s.auditEvent(base.WorkspaceID, base.ID, subject, "kb.official_edit.saved", "knowledge_base", base.ID, "Saved Official knowledge base revision", kbJSONString(map[string]string{
		"previous_revision": input.PublishedRevision, "published_revision": input.WorkingRevision,
	}))
	saved, err := s.store.SaveKnowledgeBaseOfficialEdit(ctx, storekb.SaveKnowledgeBaseOfficialEditParams{
		WorkspaceID: base.WorkspaceID, KnowledgeBaseID: base.ID,
		WorkingRevision: input.WorkingRevision, PublishedRevision: input.PublishedRevision,
		ExpectedContentDigest: input.ContentDigest,
		ExpectedVersion:       input.ExpectedVersion, ClientRequestID: input.ClientRequestID,
		ActorID: subject, AuditEvent: audit, Now: now,
	})
	if errors.Is(err, storekb.ErrOfficialEditConflict) {
		return storekb.KnowledgeBase{}, codedConflict("kb-official-edit-version-conflict", "The Official knowledge base changed before it could be saved", err)
	}
	if errors.Is(err, storekb.ErrOfficialEditReplayConflict) {
		return storekb.KnowledgeBase{}, codedConflict("kb-official-edit-replay-conflict", "The Official save request ID is already bound to another revision", err)
	}
	if err != nil {
		return storekb.KnowledgeBase{}, err
	}
	s.decorateKnowledgeBaseAccess(ctx, &saved, subject)
	decorated := []storekb.KnowledgeBase{saved}
	if err := s.store.DecorateKnowledgeBaseTaxonomy(ctx, workspaceID, decorated); err != nil {
		return storekb.KnowledgeBase{}, err
	}
	saved = decorated[0]
	saved.CanEditOfficial = true
	return saved, nil
}

func (s *KBService) requireOfficialEditor(ctx context.Context, base storekb.KnowledgeBase, subject string) error {
	if s.knowledgeBaseRole(ctx, base, subject) == "owner" {
		return nil
	}
	allowed, err := s.canAdministerWorkspace(ctx, base.WorkspaceID, subject)
	if err != nil {
		return err
	}
	if !allowed {
		return forbidden("Official editing requires knowledge base owner or workspace owner/admin access")
	}
	return nil
}

func (s *KBService) requireOfficialToDraftConverter(ctx context.Context, base storekb.KnowledgeBase, subject string) error {
	if !roleCanManage(s.knowledgeBaseRole(ctx, base, subject)) {
		return forbidden("Official-to-Draft conversion requires knowledge base management access")
	}
	allowed, err := s.canAdministerWorkspace(ctx, base.WorkspaceID, subject)
	if err != nil {
		return err
	}
	if !allowed {
		return forbidden("Official-to-Draft conversion requires workspace owner or admin access")
	}
	return nil
}

func officialSnapshotChangeFields(publishedJSON string, working kbgovernance.PublicationSnapshot) ([]string, error) {
	published, err := kbgovernance.DecodePublicationSnapshot([]byte(publishedJSON))
	if err != nil {
		return nil, fmt.Errorf("decode Official published revision snapshot: %w", err)
	}
	if published.KnowledgeBaseID != working.KnowledgeBaseID {
		return nil, fmt.Errorf("Official published revision snapshot does not match the knowledge base")
	}
	changes := make([]string, 0, 7)
	if published.Name != working.Name {
		changes = append(changes, "name")
	}
	if published.Description != working.Description {
		changes = append(changes, "description")
	}
	if published.GroupID != working.GroupID {
		changes = append(changes, "group")
	}
	publishedTags, workingTags := append([]string(nil), published.TagIDs...), append([]string(nil), working.TagIDs...)
	sort.Strings(publishedTags)
	sort.Strings(workingTags)
	if !reflect.DeepEqual(publishedTags, workingTags) {
		changes = append(changes, "tags")
	}
	if officialSourcesChanged(published.Sources, working.Sources) {
		changes = append(changes, "sources")
	}
	if officialArtifactChanged(published.Sources, working.Sources, true) {
		changes = append(changes, "wiki")
	}
	if officialArtifactChanged(published.Sources, working.Sources, false) {
		changes = append(changes, "dashboard")
	}
	return changes, nil
}

func officialSourcesChanged(left, right []kbgovernance.PublicationSnapshotSource) bool {
	withoutArtifacts := func(rows []kbgovernance.PublicationSnapshotSource) []kbgovernance.PublicationSnapshotSource {
		copyRows := append([]kbgovernance.PublicationSnapshotSource(nil), rows...)
		for index := range copyRows {
			copyRows[index].WikiRevision, copyRows[index].DashboardRevision = "", ""
			copyRows[index].WikiDisposition, copyRows[index].DashboardDisposition = "", ""
		}
		return copyRows
	}
	return !reflect.DeepEqual(withoutArtifacts(left), withoutArtifacts(right))
}

func officialArtifactChanged(left, right []kbgovernance.PublicationSnapshotSource, wiki bool) bool {
	type artifact struct{ ID, Revision, Disposition string }
	project := func(rows []kbgovernance.PublicationSnapshotSource) []artifact {
		result := make([]artifact, 0, len(rows))
		for _, row := range rows {
			if wiki {
				result = append(result, artifact{row.ID, row.WikiRevision, row.WikiDisposition})
			} else {
				result = append(result, artifact{row.ID, row.DashboardRevision, row.DashboardDisposition})
			}
		}
		return result
	}
	return !reflect.DeepEqual(project(left), project(right))
}
