package kb

import (
	"context"
	"encoding/json"
	"net/url"
	"regexp"
	"strings"

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

const (
	KBSourceImportProgressStateIdle      = "idle"
	KBSourceImportProgressStateQueued    = "queued"
	KBSourceImportProgressStateRunning   = "running"
	KBSourceImportProgressStateSucceeded = "succeeded"
	KBSourceImportProgressStateFailed    = "failed"

	KBSourceImportProgressPhaseProvisioning = "provisioning"
	KBSourceImportProgressPhaseMirrorSync   = "mirror_sync"
	KBSourceImportProgressPhaseAnalysis     = "analysis"
	KBSourceImportProgressPhaseIndexing     = "indexing"

	KBSourceImportProgressSeverityInfo    = "info"
	KBSourceImportProgressSeveritySuccess = "success"
	KBSourceImportProgressSeverityWarning = "warning"
	KBSourceImportProgressSeverityError   = "error"

	KBSourceImportProgressStepSourceCreated      = "source_created"
	KBSourceImportProgressStepRepositoriesReady  = "repositories_ready"
	KBSourceImportProgressStepMirrorSynced       = "mirror_synced"
	KBSourceImportProgressStepGeneratedKnowledge = "generated_knowledge"
	KBSourceImportProgressStepIndexed            = "indexed"
	KBSourceImportProgressStepStatusPending      = "pending"
	KBSourceImportProgressStepStatusRunning      = "running"
	KBSourceImportProgressStepStatusSucceeded    = "succeeded"
	KBSourceImportProgressStepStatusWarning      = "warning"
	KBSourceImportProgressStepStatusFailed       = "failed"
	KBSourceImportProgressStepStatusSkipped      = "skipped"
)

type KBJobDisplayProgress struct {
	Stage     string
	Message   string
	PageCount int
}

var (
	kbDisplayAuthorizationPattern = regexp.MustCompile(`(?i)\b(authorization\s*[:=]\s*)(?:(bearer|token)\s+)?\S+`)
	kbDisplayLocalPathPattern     = regexp.MustCompile(`(?i)(/tmp(?:/[^\s]+)?|/var/tmp(?:/[^\s]+)?|/private/tmp(?:/[^\s]+)?|/home/[^\s]+|/Users/[^\s]+|[A-Za-z]:\\[^\s]+)`)
	kbDisplayURLPattern           = regexp.MustCompile(`https?://[^\s]+`)
	kbDisplayCredentialPattern    = regexp.MustCompile(`(?i)\b([A-Za-z0-9_-]*(?:password|token|secret)[A-Za-z0-9_-]*|credential[_-]?ref)(\s*[:=]\s*)\S+`)
)

func (s *KBService) withSourceImportProgress(ctx context.Context, sources []kb.Source) ([]kb.Source, error) {
	if len(sources) == 0 || s == nil || s.store == nil {
		return sources, nil
	}
	workspaceID := strings.TrimSpace(sources[0].WorkspaceID)
	kbID := strings.TrimSpace(sources[0].KnowledgeBaseID)
	sourceIDs := make([]string, 0, len(sources))
	for _, source := range sources {
		if strings.TrimSpace(source.WorkspaceID) != workspaceID || strings.TrimSpace(source.KnowledgeBaseID) != kbID {
			continue
		}
		sourceIDs = append(sourceIDs, source.ID)
	}
	if len(sourceIDs) == 0 {
		return sources, nil
	}
	activeJobs, err := s.store.ListActiveSourceJobsForSources(ctx, workspaceID, kbID, sourceIDs, kbImportProgressJobTypes())
	if err != nil {
		return nil, err
	}
	terminalJobs, err := s.store.ListLatestTerminalSourceJobsForSources(ctx, workspaceID, kbID, sourceIDs, kbImportProgressJobTypes())
	if err != nil {
		return nil, err
	}
	activeBySource := map[string][]kb.Job{}
	for _, job := range activeJobs {
		activeBySource[job.SourceID] = append(activeBySource[job.SourceID], job)
	}
	terminalBySource := map[string]kb.Job{}
	for _, job := range terminalJobs {
		terminalBySource[job.SourceID] = job
	}
	for index := range sources {
		progress := sourceImportProgress(sources[index], activeBySource[sources[index].ID], terminalBySource)
		sources[index].ImportProgress = &progress
	}
	return sources, nil
}

func kbImportProgressJobTypes() []string {
	return []string{KBJobTypeSyncSourceMirror}
}

func sourceImportProgress(source kb.Source, activeJobs []kb.Job, terminalBySource map[string]kb.Job) kb.SourceImportProgress {
	terminalJob, hasTerminalJob := terminalBySource[source.ID]
	if activeJob := latestJob(activeJobs); activeJob != nil {
		// Once a mirror source has been successfully imported at least once,
		// subsequent background syncs must not regress import progress to a
		// partial "importing" state. The initial import is complete; ongoing
		// sync activity is reflected through the mirror's SyncStatus field,
		// not through import progress. This prevents the UI from showing a
		// misleading "10% importing" banner every time a scheduled re-sync
		// queues up behind other work.
		if !sourceMirrorInitiallyImported(source) {
			return withImportProgressSteps(source, activeJobs, terminalJob, importProgressFromJob(*activeJob))
		}
	}
	if hasTerminalJob && terminalJob.Status == KBJobStatusFailed && !subsequentMirrorSyncFailure(source, terminalJob) {
		return withImportProgressSteps(source, activeJobs, terminalJob, importProgressFromJob(terminalJob))
	}
	if failed, ok := sourceFailureImportProgress(source); ok && !sourceMirrorInitiallyImported(source) {
		return withImportProgressSteps(source, activeJobs, terminalJob, failed)
	}
	if warning, ok := sourceWarningImportProgress(source); ok {
		return withImportProgressSteps(source, activeJobs, terminalJob, warning)
	}
	if hasTerminalJob && terminalJob.Status == KBJobStatusSucceeded && sourceUsableForImportProgress(source, terminalJob) {
		return withImportProgressSteps(source, activeJobs, terminalJob, importProgressFromJob(terminalJob))
	}
	if progress, ok := sourceProvisioningImportProgress(source); ok {
		return withImportProgressSteps(source, activeJobs, terminalJob, progress)
	}
	return kb.SourceImportProgress{State: KBSourceImportProgressStateIdle}
}

func importProgressFromJob(job kb.Job) kb.SourceImportProgress {
	display := KBJobDisplayProgressForJob(job)
	state := importProgressStateForJobStatus(job.Status)
	stage := firstNonEmptyString(display.Stage, defaultStageForImportProgressState(state))
	phase := importProgressPhaseForJob(job.JobType, stage)
	message := display.Message
	if state == KBSourceImportProgressStateFailed {
		message = ""
	}
	return kb.SourceImportProgress{
		State:          state,
		Severity:       importProgressSeverityForState(state),
		Phase:          phase,
		Stage:          stage,
		Message:        message,
		Percent:        importProgressPercent(phase, stage, state),
		CurrentJobID:   job.ID,
		CurrentJobType: job.JobType,
		StartedAt:      job.StartedAt,
		UpdatedAt:      &job.UpdatedAt,
		FinishedAt:     job.FinishedAt,
	}
}

func sourceFailureImportProgress(source kb.Source) (kb.SourceImportProgress, bool) {
	if !sourceHasBlockingFailure(source) {
		return kb.SourceImportProgress{}, false
	}
	phase := KBSourceImportProgressPhaseAnalysis
	if sourceStatusFailed(source.SyncStatus) || (source.Mirror != nil && (sourceStatusFailed(source.Mirror.SyncStatus) || strings.TrimSpace(source.Mirror.LastError) != "")) {
		phase = KBSourceImportProgressPhaseMirrorSync
	} else if sourceStatusFailed(source.IndexStatus) {
		phase = KBSourceImportProgressPhaseIndexing
	}
	updatedAt := source.UpdatedAt
	return kb.SourceImportProgress{
		State:     KBSourceImportProgressStateFailed,
		Severity:  KBSourceImportProgressSeverityError,
		Phase:     phase,
		Stage:     KBSourceStatusFailed,
		UpdatedAt: &updatedAt,
	}, true
}

func sourceWarningImportProgress(source kb.Source) (kb.SourceImportProgress, bool) {
	if !sourceReadyEnoughForOptionalWarnings(source) || !sourceStatusFailed(source.AnalysisStatus) {
		return kb.SourceImportProgress{}, false
	}
	updatedAt := source.UpdatedAt
	return kb.SourceImportProgress{
		State:     KBSourceImportProgressStateSucceeded,
		Severity:  KBSourceImportProgressSeverityWarning,
		Phase:     KBSourceImportProgressPhaseAnalysis,
		Stage:     KBSourceStatusFailed,
		UpdatedAt: &updatedAt,
	}, true
}

func sourceProvisioningImportProgress(source kb.Source) (kb.SourceImportProgress, bool) {
	if strings.TrimSpace(source.Status) != KBSourceStatusQueued {
		return kb.SourceImportProgress{}, false
	}
	updatedAt := source.UpdatedAt
	return kb.SourceImportProgress{
		State:     KBSourceImportProgressStateQueued,
		Severity:  KBSourceImportProgressSeverityInfo,
		Phase:     KBSourceImportProgressPhaseProvisioning,
		Stage:     KBSourceStatusQueued,
		Percent:   importProgressPercent(KBSourceImportProgressPhaseProvisioning, KBSourceStatusQueued, KBSourceImportProgressStateQueued),
		UpdatedAt: &updatedAt,
	}, true
}

func withImportProgressSteps(source kb.Source, activeJobs []kb.Job, terminalJob kb.Job, progress kb.SourceImportProgress) kb.SourceImportProgress {
	if strings.TrimSpace(progress.Severity) == "" {
		progress.Severity = importProgressSeverityForState(progress.State)
	}
	if strings.TrimSpace(progress.State) == KBSourceImportProgressStateIdle {
		return progress
	}
	progress.Steps = sourceImportProgressSteps(source, activeJobs, terminalJob)
	progress.Severity = highestImportProgressSeverity(progress.Severity, progress.Steps)
	return progress
}

func sourceImportProgressSteps(source kb.Source, activeJobs []kb.Job, terminalJob kb.Job) []kb.SourceImportProgressStep {
	activeByType := latestJobsByType(activeJobs)
	terminalJobType := strings.TrimSpace(terminalJob.JobType)
	steps := []kb.SourceImportProgressStep{
		{
			ID:     KBSourceImportProgressStepSourceCreated,
			Phase:  KBSourceImportProgressPhaseProvisioning,
			Status: KBSourceImportProgressStepStatusSucceeded,
		},
		repositoriesReadyProgressStep(source),
		mirrorSyncedProgressStep(source, activeByType[KBJobTypeSyncSourceMirror], terminalJobForType(terminalJob, terminalJobType, KBJobTypeSyncSourceMirror)),
	}
	return steps
}

func repositoriesReadyProgressStep(source kb.Source) kb.SourceImportProgressStep {
	step := kb.SourceImportProgressStep{
		ID:     KBSourceImportProgressStepRepositoriesReady,
		Phase:  KBSourceImportProgressPhaseProvisioning,
		Status: KBSourceImportProgressStepStatusSucceeded,
	}
	if sourceProvisioningBlocked(source) {
		step.Status = KBSourceImportProgressStepStatusFailed
		return step
	}
	if strings.TrimSpace(source.Status) == KBSourceStatusQueued && strings.TrimSpace(source.SyncStatus) == KBSourceStatusQueued && strings.TrimSpace(source.IndexStatus) == KBSourceStatusQueued {
		step.Status = KBSourceImportProgressStepStatusRunning
	}
	return step
}

func mirrorSyncedProgressStep(source kb.Source, activeJob *kb.Job, terminalJob *kb.Job) kb.SourceImportProgressStep {
	step := kb.SourceImportProgressStep{
		ID:       KBSourceImportProgressStepMirrorSynced,
		Phase:    KBSourceImportProgressPhaseMirrorSync,
		Optional: true,
		Status:   KBSourceImportProgressStepStatusSkipped,
	}
	if source.Mirror == nil {
		return step
	}
	step.Optional = false
	if !source.Mirror.Enabled || strings.TrimSpace(source.Mirror.SyncStatus) == kb.SourceMirrorSyncStatusDisabled {
		step.Optional = true
		return step
	}
	if activeJob != nil {
		return stepFromJob(step.ID, step.Phase, false, *activeJob)
	}
	if terminalJob != nil && terminalJob.Status == KBJobStatusFailed {
		step.Status = KBSourceImportProgressStepStatusFailed
		step.UpdatedAt = &terminalJob.UpdatedAt
		step.FinishedAt = terminalJob.FinishedAt
		return step
	}
	if sourceStatusFailed(source.SyncStatus) || sourceStatusFailed(source.Mirror.SyncStatus) || strings.TrimSpace(source.Mirror.LastError) != "" {
		step.Status = KBSourceImportProgressStepStatusFailed
		updatedAt := source.Mirror.UpdatedAt
		step.UpdatedAt = &updatedAt
		return step
	}
	if terminalJob != nil && terminalJob.Status == KBJobStatusSucceeded || sourceStatusReady(source.SyncStatus) || strings.TrimSpace(source.Mirror.SyncStatus) == kb.SourceMirrorSyncStatusSuccess {
		step.Status = KBSourceImportProgressStepStatusSucceeded
		return step
	}
	step.Status = statusProgressStepStatus(firstNonEmptyString(source.Mirror.SyncStatus, source.SyncStatus))
	return step
}

func stepFromJob(id, phase string, optional bool, job kb.Job) kb.SourceImportProgressStep {
	display := KBJobDisplayProgressForJob(job)
	step := kb.SourceImportProgressStep{
		ID:         id,
		Phase:      phase,
		Stage:      firstNonEmptyString(display.Stage, defaultStageForImportProgressState(importProgressStateForJobStatus(job.Status))),
		Status:     jobProgressStepStatus(job.Status),
		Optional:   optional,
		Message:    display.Message,
		StartedAt:  job.StartedAt,
		UpdatedAt:  &job.UpdatedAt,
		FinishedAt: job.FinishedAt,
	}
	return step
}

func latestJobsByType(jobs []kb.Job) map[string]*kb.Job {
	out := map[string]*kb.Job{}
	for index := range jobs {
		job := &jobs[index]
		jobType := strings.TrimSpace(job.JobType)
		if jobType == "" {
			continue
		}
		if existing := out[jobType]; existing == nil || jobAfter(*job, *existing) {
			out[jobType] = job
		}
	}
	return out
}

func terminalJobForType(job kb.Job, jobType string, expected string) *kb.Job {
	if jobType != expected {
		return nil
	}
	return &job
}

func jobProgressStepStatus(status string) string {
	switch strings.TrimSpace(status) {
	case KBJobStatusQueued:
		return KBSourceImportProgressStepStatusPending
	case KBJobStatusRunning:
		return KBSourceImportProgressStepStatusRunning
	case KBJobStatusSucceeded:
		return KBSourceImportProgressStepStatusSucceeded
	case KBJobStatusFailed:
		return KBSourceImportProgressStepStatusFailed
	default:
		return KBSourceImportProgressStepStatusPending
	}
}

func statusProgressStepStatus(status string) string {
	switch strings.TrimSpace(status) {
	case KBJobStatusRunning, "indexing":
		return KBSourceImportProgressStepStatusRunning
	case KBJobStatusQueued, "pending":
		return KBSourceImportProgressStepStatusPending
	case KBJobStatusSucceeded, "ready", "success":
		return KBSourceImportProgressStepStatusSucceeded
	case KBJobStatusFailed, "error":
		return KBSourceImportProgressStepStatusFailed
	default:
		return KBSourceImportProgressStepStatusPending
	}
}

func sourceGeneratedKnowledgeApplies(source kb.Source) bool {
	sourceType := strings.TrimSpace(source.SourceType)
	if sourceType == "codebase" || sourceType == "file_repository" {
		return true
	}
	return strings.TrimSpace(source.AnalysisStatus) != "" && strings.TrimSpace(source.AnalysisStatus) != "pending"
}

func sourceProvisioningBlocked(source kb.Source) bool {
	return sourceStatusFailed(source.Status) &&
		!sourceStatusFailed(source.SyncStatus) &&
		!sourceStatusFailed(source.IndexStatus) &&
		(source.Mirror == nil || !sourceStatusFailed(source.Mirror.SyncStatus))
}

func importProgressSeverityForState(state string) string {
	switch state {
	case KBSourceImportProgressStateQueued, KBSourceImportProgressStateRunning:
		return KBSourceImportProgressSeverityInfo
	case KBSourceImportProgressStateSucceeded:
		return KBSourceImportProgressSeveritySuccess
	case KBSourceImportProgressStateFailed:
		return KBSourceImportProgressSeverityError
	default:
		return ""
	}
}

func highestImportProgressSeverity(current string, steps []kb.SourceImportProgressStep) string {
	severity := current
	for _, step := range steps {
		switch step.Status {
		case KBSourceImportProgressStepStatusFailed:
			if !step.Optional {
				return KBSourceImportProgressSeverityError
			}
			severity = maxImportProgressSeverity(severity, KBSourceImportProgressSeverityWarning)
		case KBSourceImportProgressStepStatusWarning:
			severity = maxImportProgressSeverity(severity, KBSourceImportProgressSeverityWarning)
		}
	}
	return severity
}

func maxImportProgressSeverity(left, right string) string {
	if importProgressSeverityRank(right) > importProgressSeverityRank(left) {
		return right
	}
	return left
}

func importProgressSeverityRank(severity string) int {
	switch severity {
	case KBSourceImportProgressSeverityError:
		return 4
	case KBSourceImportProgressSeverityWarning:
		return 3
	case KBSourceImportProgressSeveritySuccess:
		return 2
	case KBSourceImportProgressSeverityInfo:
		return 1
	default:
		return 0
	}
}

func sourceUsableForImportProgress(source kb.Source, job kb.Job) bool {
	if sourceHasBlockingFailure(source) {
		return false
	}
	switch importProgressPhaseForJob(job.JobType, "") {
	case KBSourceImportProgressPhaseMirrorSync:
		// If the mirror exists but has no success state (LastSuccessAt nil
		// and LastMirroredCommit empty), the current remote/ref config has
		// not been successfully synced yet. A stale terminal succeeded job
		// from a previous config must not be shown as "imported 100%".
		if source.Mirror != nil && source.Mirror.LastSuccessAt == nil && strings.TrimSpace(source.Mirror.LastMirroredCommit) == "" {
			return false
		}
		if sourceStatusReady(source.SyncStatus) || strings.TrimSpace(source.Status) == KBSourceStatusReady {
			return true
		}
		return source.Mirror != nil && strings.TrimSpace(source.Mirror.SyncStatus) == kb.SourceMirrorSyncStatusSuccess
	case KBSourceImportProgressPhaseAnalysis:
		return sourceStatusReady(source.Status) && sourceStatusReady(source.IndexStatus) && sourceAnalysisSucceeded(source.AnalysisStatus)
	default:
		return sourceStatusForHealth(source) == "ready"
	}
}

func sourceHasBlockingFailure(source kb.Source) bool {
	if sourceStatusFailed(source.Status) || sourceStatusFailed(source.SyncStatus) || sourceStatusFailed(source.IndexStatus) {
		return true
	}
	if source.Mirror != nil && (sourceStatusFailed(source.Mirror.SyncStatus) || strings.TrimSpace(source.Mirror.LastError) != "") {
		return true
	}
	return strings.TrimSpace(source.LastError) != "" && !sourceStatusFailed(source.AnalysisStatus)
}

func sourceReadyEnoughForOptionalWarnings(source kb.Source) bool {
	if sourceHasBlockingFailure(source) {
		return false
	}
	return sourceStatusReady(source.Status) && sourceStatusReady(source.SyncStatus) && sourceStatusReady(source.IndexStatus)
}

func sourceStatusFailed(status string) bool {
	switch strings.TrimSpace(status) {
	case "failed", "error":
		return true
	default:
		return false
	}
}

// sourceMirrorInitiallyImported reports whether a mirror source has been
// successfully synced at least once. Once the initial import is complete,
// subsequent sync activity (queued, running, or failed) is a background
// refresh, not an import, and must not regress import progress.
func sourceMirrorInitiallyImported(source kb.Source) bool {
	if source.Mirror == nil {
		return false
	}
	return source.Mirror.LastSuccessAt != nil || strings.TrimSpace(source.Mirror.LastMirroredCommit) != ""
}

// subsequentMirrorSyncFailure reports whether a failed terminal job is a
// mirror sync job for a source that has already been successfully imported.
// Such failures are background refresh failures, not import failures, and
// should not drive the import progress state to "failed".
func subsequentMirrorSyncFailure(source kb.Source, job kb.Job) bool {
	if !sourceMirrorInitiallyImported(source) {
		return false
	}
	return strings.TrimSpace(job.JobType) == KBJobTypeSyncSourceMirror
}

func sourceStatusReady(status string) bool {
	switch strings.TrimSpace(status) {
	case "ready", "success", "succeeded":
		return true
	default:
		return false
	}
}

func sourceAnalysisSucceeded(status string) bool {
	switch strings.TrimSpace(status) {
	case "", "ready", "success", "succeeded":
		return true
	default:
		return false
	}
}

func latestJob(jobs []kb.Job) *kb.Job {
	if len(jobs) == 0 {
		return nil
	}
	latest := 0
	for index := 1; index < len(jobs); index++ {
		if jobAfter(jobs[index], jobs[latest]) {
			latest = index
		}
	}
	return &jobs[latest]
}

func jobAfter(left kb.Job, right kb.Job) bool {
	if !left.UpdatedAt.Equal(right.UpdatedAt) {
		return left.UpdatedAt.After(right.UpdatedAt)
	}
	if !left.CreatedAt.Equal(right.CreatedAt) {
		return left.CreatedAt.After(right.CreatedAt)
	}
	return left.ID > right.ID
}

func importProgressStateForJobStatus(status string) string {
	switch strings.TrimSpace(status) {
	case KBJobStatusQueued:
		return KBSourceImportProgressStateQueued
	case KBJobStatusRunning:
		return KBSourceImportProgressStateRunning
	case KBJobStatusSucceeded:
		return KBSourceImportProgressStateSucceeded
	case KBJobStatusFailed:
		return KBSourceImportProgressStateFailed
	default:
		return KBSourceImportProgressStateIdle
	}
}

func defaultStageForImportProgressState(state string) string {
	switch state {
	case KBSourceImportProgressStateQueued:
		return KBSourceStatusQueued
	case KBSourceImportProgressStateRunning:
		return KBJobStatusRunning
	case KBSourceImportProgressStateSucceeded:
		return KBSourceStatusReady
	case KBSourceImportProgressStateFailed:
		return KBSourceStatusFailed
	default:
		return ""
	}
}

func importProgressPhaseForJob(jobType string, stage string) string {
	switch strings.TrimSpace(jobType) {
	case KBJobTypeSyncSourceMirror:
		return KBSourceImportProgressPhaseMirrorSync
	default:
		return ""
	}
}

func importProgressPercent(phase string, stage string, state string) *int {
	if state == KBSourceImportProgressStateFailed || state == KBSourceImportProgressStateIdle {
		return nil
	}
	if state == KBSourceImportProgressStateSucceeded {
		return intPointer(100)
	}
	progressByPhase := map[string]map[string]int{
		KBSourceImportProgressPhaseProvisioning: {
			KBSourceStatusQueued: 10,
			KBSourceStatusReady:  100,
		},
		KBSourceImportProgressPhaseMirrorSync: {
			KBSourceMirrorStageQueued:      10,
			KBSourceMirrorStageValidating:  25,
			KBSourceMirrorStageSyncingAGS:  55,
			KBSourceMirrorStageReconciling: 80,
			KBSourceMirrorStageReady:       100,
		},
	}
	if stages := progressByPhase[phase]; stages != nil {
		if percent, ok := stages[strings.TrimSpace(stage)]; ok {
			return intPointer(percent)
		}
	}
	return nil
}

func intPointer(value int) *int {
	return &value
}

func KBJobDisplayProgressForJob(job kb.Job) KBJobDisplayProgress {
	switch strings.TrimSpace(job.JobType) {
	case KBJobTypeSyncSourceMirror:
		return KBMirrorJobDisplayProgress(job.Progress)
	default:
		return KBJobDisplayProgress{}
	}
}

func KBAnalysisRunDisplayProgress(run kb.AnalysisRun) KBJobDisplayProgress {
	if strings.TrimSpace(run.Progress) == "" {
		return KBJobDisplayProgress{}
	}
	return KBRepositoryCodeWikiDisplayProgress(run.Progress)
}

func KBMirrorJobDisplayProgress(raw string) KBJobDisplayProgress {
	if strings.TrimSpace(raw) == "" {
		return KBJobDisplayProgress{}
	}
	var payload struct {
		Status  string `json:"status"`
		Stage   string `json:"stage"`
		Message string `json:"message"`
	}
	if err := json.Unmarshal([]byte(raw), &payload); err != nil {
		return KBJobDisplayProgress{}
	}
	return KBJobDisplayProgress{
		Stage:   firstNonEmptyString(payload.Stage, payload.Status),
		Message: SanitizeKBProgressMessage(payload.Message),
	}
}

func KBRepositoryCodeWikiDisplayProgress(raw string) KBJobDisplayProgress {
	if strings.TrimSpace(raw) == "" {
		return KBJobDisplayProgress{}
	}
	var payload struct {
		Stage     string `json:"stage"`
		Message   string `json:"message"`
		PageCount int    `json:"page_count"`
	}
	if err := json.Unmarshal([]byte(raw), &payload); err != nil {
		return KBJobDisplayProgress{}
	}
	return KBJobDisplayProgress{
		Stage:     strings.TrimSpace(payload.Stage),
		Message:   SanitizeKBProgressMessage(payload.Message),
		PageCount: payload.PageCount,
	}
}

func SanitizeKBProgressMessage(message string) string {
	message = strings.TrimSpace(message)
	if message == "" {
		return ""
	}
	message = kbDisplayURLPattern.ReplaceAllStringFunc(message, redactKBDisplayURL)
	message = kbDisplayLocalPathPattern.ReplaceAllString(message, "[local path]")
	message = kbDisplayAuthorizationPattern.ReplaceAllStringFunc(message, redactKBAuthorizationHeader)
	message = kbDisplayCredentialPattern.ReplaceAllString(message, "${1}${2}[redacted]")
	message = strings.Join(strings.Fields(message), " ")
	const maxRunes = 500
	runes := []rune(message)
	if len(runes) > maxRunes {
		message = string(runes[:maxRunes]) + "..."
	}
	return message
}

func redactKBAuthorizationHeader(match string) string {
	parts := kbDisplayAuthorizationPattern.FindStringSubmatch(match)
	if len(parts) == 0 {
		return "[redacted]"
	}
	prefix := parts[1]
	scheme := ""
	if len(parts) > 2 {
		scheme = strings.TrimSpace(parts[2])
	}
	if scheme != "" {
		return prefix + scheme + " [redacted]"
	}
	return prefix + "[redacted]"
}

func redactKBDisplayURL(raw string) string {
	parsed, err := url.Parse(raw)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return raw
	}
	parsed.User = nil
	query := parsed.Query()
	for key := range query {
		lower := strings.ToLower(key)
		if strings.Contains(lower, "token") || strings.Contains(lower, "secret") || strings.Contains(lower, "password") || strings.Contains(lower, "key") {
			query.Set(key, "redacted")
		}
	}
	parsed.RawQuery = query.Encode()
	return parsed.String()
}
