package kb

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

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

const (
	KBJobTypeGenerateWiki = "generate_source_wiki"
	maxWikiReasonBytes    = 4000
)

// KBWikiGenerator produces an immutable Wiki revision for one exact Source
// revision. Implementations may stage external data, but only the service's
// fenced completion transaction advances the reader-visible pointer.
type KBWikiGenerator interface {
	Generate(context.Context, KBWikiGenerationInput) (KBWikiGenerationOutput, error)
}

type KBWikiGenerationInput struct {
	WorkspaceID     string
	KnowledgeBaseID string
	SourceID        string
	SourceRevision  string
	SourceCommitSHA string
	Scope           string
	Language        string
	RequestID       string
}

type KBWikiGenerationOutput struct {
	InsightsCommitSHA   string
	PublishedWikiCommit string
}

type KBWikiGenerationMutation struct {
	SourceRevision string
	Scope          string
	Language       string
	Reason         string
	IdempotencyKey string
}

type WikiGenerationRunOptions struct {
	Limit         int
	LeaseOwner    string
	LeaseDuration time.Duration
	WorkspaceIDs  []string
}

type wikiGenerationJobPayload struct {
	RequestID      string `json:"request_id"`
	SourceRevision string `json:"source_revision"`
}

func (s *KBService) WikiGenerationEnabled() bool {
	return s != nil && s.wikiGenerator != nil
}

// RequestDirectWikiGeneration is the Admin path. Member requests must enter
// through the AgentTeam Approval adapter and therefore never fall back to this
// method merely because they have KB edit access.
func (s *KBService) RequestDirectWikiGeneration(ctx context.Context, workspaceID, subject, kbID, sourceID string, input KBWikiGenerationMutation) (KBSetupView, error) {
	if !s.WikiGenerationEnabled() {
		return KBSetupView{}, codedConflict("kb-wiki-generation-unavailable", "Wiki generation is not configured", nil)
	}
	base, err := s.GetKnowledgeBase(ctx, workspaceID, subject, kbID)
	if err != nil {
		return KBSetupView{}, err
	}
	if !roleCanEdit(s.knowledgeBaseRole(ctx, base, subject)) {
		return KBSetupView{}, forbidden("Wiki generation requires knowledge base edit access")
	}
	allowed, err := s.canAdministerWorkspace(ctx, workspaceID, subject)
	if err != nil {
		return KBSetupView{}, err
	}
	if !allowed {
		return KBSetupView{}, forbidden("Direct Wiki generation requires workspace owner or admin access")
	}
	source, err := s.store.GetSource(ctx, workspaceID, kbID, sourceID)
	if err != nil {
		return KBSetupView{}, mapStoreNotFound(err)
	}
	native, err := s.kbSourceNativeActive(ctx, workspaceID, source.ID)
	if err != nil {
		return KBSetupView{}, err
	}
	if native {
		return KBSetupView{}, nativeKBSourceLegacyOperationConflict()
	}
	revision, err := s.store.GetCurrentSourceRevision(ctx, workspaceID, kbID, source.ID)
	if err != nil {
		return KBSetupView{}, mapStoreNotFound(err)
	}
	input, err = normalizeWikiGenerationMutation(input)
	if err != nil {
		return KBSetupView{}, err
	}
	if revision.ID != input.SourceRevision {
		return KBSetupView{}, codedConflict("kb-wiki-source-revision-stale", "The Source revision changed; refresh before requesting Wiki generation", nil)
	}
	payload := map[string]string{
		"workspaceId": workspaceID, "knowledgeBaseId": kbID,
		"sourceId": source.ID, "sourceRevision": revision.ID,
		"sourceCommitSha": revision.SourceCommitSHA, "scope": input.Scope,
		"language": input.Language, "reason": input.Reason,
		"requesterId": subject, "idempotencyKey": input.IdempotencyKey,
	}
	payloadJSON, err := json.Marshal(payload)
	if err != nil {
		return KBSetupView{}, fmt.Errorf("encode Wiki generation request: %w", err)
	}
	digest := sha256.Sum256(payloadJSON)
	now := s.now()
	requestID := newKBID("kbgr")
	jobID := newKBID("job")
	jobPayload, err := json.Marshal(wikiGenerationJobPayload{RequestID: requestID, SourceRevision: revision.ID})
	if err != nil {
		return KBSetupView{}, fmt.Errorf("encode Wiki generation job: %w", err)
	}
	request := storekb.KnowledgeBaseGenerationRequest{
		ID: requestID, WorkspaceID: workspaceID, KnowledgeBaseID: kbID,
		SourceID: source.ID, Kind: storekb.GenerationKindWiki,
		SourceRevisionID: revision.ID, SourceCommitSHA: revision.SourceCommitSHA,
		Scope: input.Scope, Language: input.Language, Reason: input.Reason,
		RequesterID: subject, Status: storekb.GenerationRequestStatusQueued,
		JobID: jobID, IdempotencyKey: input.IdempotencyKey,
		PayloadDigest: hex.EncodeToString(digest[:]), Version: 1,
		CreatedAt: now, UpdatedAt: now,
	}
	job := storekb.Job{
		ID: jobID, WorkspaceID: workspaceID, KnowledgeBaseID: kbID,
		SourceID: source.ID, JobType: KBJobTypeGenerateWiki, Status: KBJobStatusQueued,
		Priority: 100, Progress: string(jobPayload), CreatedAt: now, UpdatedAt: now,
	}
	_, _, err = s.store.CreateWikiGeneration(ctx, storekb.CreateWikiGenerationParams{Request: request, Job: &job, Now: now})
	if err != nil {
		switch {
		case errors.Is(err, storekb.ErrGenerationRequestAlreadyActive):
			return KBSetupView{}, codedConflict("kb-wiki-generation-active", "Wiki generation is already active for this Source", err)
		case errors.Is(err, storekb.ErrGenerationRequestReplayConflict):
			return KBSetupView{}, codedConflict("kb-wiki-idempotency-conflict", "The Wiki request key is already bound to another payload", err)
		case errors.Is(err, storekb.ErrGenerationSourceRevisionChanged):
			return KBSetupView{}, codedConflict("kb-wiki-source-revision-stale", "The Source revision changed; refresh before requesting Wiki generation", err)
		case errors.Is(err, storekb.ErrGenerationArtifactAlreadyReady):
			return KBSetupView{}, codedConflict("kb-wiki-already-ready", "The current Source revision already has a published Wiki", err)
		default:
			return KBSetupView{}, err
		}
	}
	return s.GetKnowledgeBaseSetup(ctx, workspaceID, subject, kbID)
}

func normalizeWikiGenerationMutation(input KBWikiGenerationMutation) (KBWikiGenerationMutation, error) {
	input.SourceRevision = strings.TrimSpace(input.SourceRevision)
	input.Scope = strings.TrimSpace(input.Scope)
	input.Language = strings.TrimSpace(input.Language)
	input.Reason = strings.TrimSpace(input.Reason)
	input.IdempotencyKey = strings.TrimSpace(input.IdempotencyKey)
	if input.Scope == "" {
		input.Scope = "full"
	}
	if input.Language == "" {
		input.Language = "auto"
	}
	if input.SourceRevision == "" || input.IdempotencyKey == "" {
		return KBWikiGenerationMutation{}, invalidArgument("Source revision and idempotency key are required")
	}
	if input.Scope != "full" {
		return KBWikiGenerationMutation{}, invalidArgument("Wiki generation scope must be full")
	}
	if len(input.IdempotencyKey) > 128 || len(input.Scope) > 64 || len(input.Language) > 32 || len(input.Reason) > maxWikiReasonBytes {
		return KBWikiGenerationMutation{}, invalidArgument("Wiki generation request exceeds field limits")
	}
	return input, nil
}

// RunPendingWikiGenerationJobs executes explicitly requested work only. It
// never scans Sources or invents generation requests from mirror state.
func (s *KBService) RunPendingWikiGenerationJobs(ctx context.Context, options WikiGenerationRunOptions) (int, error) {
	if !s.WikiGenerationEnabled() {
		return 0, nil
	}
	if options.Limit <= 0 {
		options.Limit = 1
	}
	if strings.TrimSpace(options.LeaseOwner) == "" {
		return 0, invalidArgument("Wiki generation lease owner is required")
	}
	if options.LeaseDuration <= 0 {
		options.LeaseDuration = 2*time.Hour + 5*time.Minute
	}
	jobs, err := s.store.ClaimQueuedJobsFair(ctx, KBJobTypeGenerateWiki, options.LeaseOwner, options.LeaseDuration, options.Limit, s.now(), options.WorkspaceIDs)
	if err != nil {
		return 0, err
	}
	var runErr error
	for _, job := range jobs {
		if err := s.runWikiGenerationJob(ctx, job, options.LeaseOwner); err != nil {
			runErr = errors.Join(runErr, fmt.Errorf("Wiki generation job %s: %w", job.ID, err))
		}
	}
	return len(jobs), runErr
}

func (s *KBService) runWikiGenerationJob(ctx context.Context, job storekb.Job, leaseOwner string) error {
	request, found, err := s.store.FindGenerationRequestByJob(ctx, job.WorkspaceID, job.ID)
	if err != nil {
		return err
	}
	if !found {
		return s.cancelUnboundWikiGenerationJob(ctx, job, leaseOwner)
	}
	var payload wikiGenerationJobPayload
	payloadErr := json.Unmarshal([]byte(job.Progress), &payload)
	if payloadErr == nil && (payload.RequestID != request.ID || payload.SourceRevision != request.SourceRevisionID) {
		payloadErr = errors.New("Wiki generation job payload does not match its durable request")
	}
	request, running, err := s.store.MarkWikiGenerationRunning(ctx, job.WorkspaceID, request.ID, job.ID, s.now())
	if err != nil {
		return err
	}
	if !running {
		return s.cancelUnboundWikiGenerationJob(ctx, job, leaseOwner)
	}
	if payloadErr != nil {
		return s.finishWikiGenerationJobWithOutput(ctx, job, leaseOwner, request, KBWikiGenerationOutput{}, payloadErr, "")
	}
	base, err := s.store.GetKnowledgeBase(ctx, request.WorkspaceID, request.KnowledgeBaseID)
	if err != nil {
		return s.finishWikiGenerationJobWithOutput(ctx, job, leaseOwner, request, KBWikiGenerationOutput{}, nil, "knowledge_base_unavailable")
	}
	allowed, authorizationErr := s.wikiGenerationRequesterAuthorized(ctx, base, request)
	if authorizationErr != nil {
		return s.finishWikiGenerationJobWithOutput(ctx, job, leaseOwner, request, KBWikiGenerationOutput{}, authorizationErr, "")
	}
	if !allowed {
		return s.finishWikiGenerationJobWithOutput(ctx, job, leaseOwner, request, KBWikiGenerationOutput{}, nil, "authorization_changed")
	}
	native, authorityErr := s.kbSourceNativeActive(ctx, request.WorkspaceID, request.SourceID)
	if authorityErr != nil {
		return s.finishWikiGenerationJobWithOutput(ctx, job, leaseOwner, request, KBWikiGenerationOutput{}, authorityErr, "")
	}
	if native {
		return s.finishWikiGenerationJobWithOutput(ctx, job, leaseOwner, request, KBWikiGenerationOutput{}, nil, "native_authority_active")
	}
	output, generationErr := s.wikiGenerator.Generate(ctx, KBWikiGenerationInput{
		WorkspaceID: request.WorkspaceID, KnowledgeBaseID: request.KnowledgeBaseID,
		SourceID: request.SourceID, SourceRevision: request.SourceRevisionID,
		SourceCommitSHA: request.SourceCommitSHA, Scope: request.Scope,
		Language: request.Language, RequestID: request.ID,
	})
	if generationErr == nil && strings.TrimSpace(output.PublishedWikiCommit) == "" {
		generationErr = fmt.Errorf("Wiki generator returned no published revision")
	}
	return s.finishWikiGenerationJobWithOutput(ctx, job, leaseOwner, request, output, generationErr, "")
}

func (s *KBService) wikiGenerationRequesterAuthorized(ctx context.Context, base storekb.KnowledgeBase, request storekb.KnowledgeBaseGenerationRequest) (bool, error) {
	if !roleCanEdit(s.knowledgeBaseRole(ctx, base, request.RequesterID)) {
		return false, nil
	}
	if strings.TrimSpace(request.ApprovalID) == "" {
		return s.canAdministerWorkspace(ctx, request.WorkspaceID, request.RequesterID)
	}
	if s.principalWorkspaceAccessVerifier == nil {
		return false, invalidArgument("approved Wiki generation requires workspace membership verification")
	}
	return s.principalWorkspaceAccessVerifier.HasWorkspaceAccess(ctx, request.WorkspaceID, request.RequesterID)
}

func (s *KBService) finishWikiGenerationJobWithOutput(ctx context.Context, job storekb.Job, leaseOwner string, request storekb.KnowledgeBaseGenerationRequest, output KBWikiGenerationOutput, failure error, staleReason string) error {
	now := s.now()
	audit := storekb.AuditEvent{
		ID: newKBID("audit"), WorkspaceID: request.WorkspaceID,
		KnowledgeBaseID: request.KnowledgeBaseID, ActorID: request.RequesterID,
		Action: "kb.wiki.generated", ResourceType: "source", ResourceID: request.SourceID,
		Summary: "Published generated Wiki", Details: kbJSONString(map[string]string{
			"request_id": request.ID, "source_revision": request.SourceRevisionID,
			"wiki_revision": output.PublishedWikiCommit,
		}), CreatedAt: now,
	}
	_, err := s.store.FinishWikiGeneration(ctx, storekb.FinishWikiGenerationParams{
		WorkspaceID: request.WorkspaceID, RequestID: request.ID, JobID: job.ID,
		LeaseOwner: leaseOwner, AttemptCount: job.AttemptCount,
		InsightsCommitSHA:   output.InsightsCommitSHA,
		PublishedWikiCommit: output.PublishedWikiCommit,
		Failure:             failure, StaleReason: staleReason, AuditEvent: audit, Now: now,
	})
	return err
}

func (s *KBService) cancelUnboundWikiGenerationJob(ctx context.Context, job storekb.Job, leaseOwner string) error {
	now := s.now()
	job.Status, job.Error, job.LeaseOwner, job.LeaseUntil = KBJobStatusCancelled, "", "", nil
	job.FinishedAt, job.UpdatedAt = &now, now
	_, updated, err := s.store.UpdateLeasedJob(ctx, job, leaseOwner, job.AttemptCount)
	if err != nil {
		return err
	}
	if !updated {
		return conflict("Wiki generation job lease was lost")
	}
	return nil
}

func (s *KBService) ReclaimStaleWikiGenerationJobs(ctx context.Context, workspaceIDs []string) (int64, error) {
	if s == nil || s.store == nil {
		return 0, invalidArgument("kb service is not configured")
	}
	return s.store.ReclaimStaleRunningJobs(ctx, KBJobTypeGenerateWiki, s.now(), workspaceIDs)
}
