package kb

import (
	"context"
	"errors"
	"log/slog"
	"net"
	"strings"
	"time"

	"github.com/go-git/go-git/v5/plumbing/transport"
	storekb "github.com/tidbcloud/AgentTeam/internal/store/kb"
)

const (
	sourceMirrorErrorUpstreamAuth    = "upstream_auth"
	sourceMirrorErrorUpstreamMissing = "upstream_not_found"
	sourceMirrorErrorDNS             = "dns"
	sourceMirrorErrorNetwork         = "network"
	sourceMirrorErrorTLS             = "tls"
	sourceMirrorErrorProxy           = "proxy"
	sourceMirrorErrorRefMissing      = "ref_not_found"
	sourceMirrorErrorCloneTimeout    = "clone_timeout"
	sourceMirrorErrorAGSPush         = "ags_push"
	sourceMirrorErrorReconcile       = "reconcile"
	sourceMirrorErrorLeaseLost       = "lease_lost"
	sourceMirrorErrorInternal        = "internal"
)

type sourceMirrorErrorDiagnostic struct {
	Code      string
	Retryable bool
}

// SourceMirrorGitProgress reports bounded Git retry progress without exposing
// repository URLs, credentials, or raw command output.
type SourceMirrorGitProgress struct {
	Operation   string
	Attempt     int
	MaxAttempts int
	Retrying    bool
	Failure     error
}

// SourceMirrorJobDiagnostics is the display-safe projection of one mirror job's
// durable progress payload. Raw progress JSON remains internal to the service.
type SourceMirrorJobDiagnostics struct {
	Ref            string
	Trigger        string
	RequestedBy    string
	TraceID        string
	FailedStage    string
	ErrorCode      string
	Retryable      *bool
	GitOperation   string
	GitAttempt     int
	GitMaxAttempts int
}

// SourceMirrorJobDiagnosticsForJob returns the safe mirror-specific fields
// persisted in a job's progress payload. Older jobs without diagnostics still
// expose their ref and use the job id as a correlation id.
func SourceMirrorJobDiagnosticsForJob(job storekb.Job) SourceMirrorJobDiagnostics {
	payload, err := parseSourceMirrorProgress(job.Progress)
	if err != nil || payload.Kind == "" {
		legacy, legacyErr := parseSourceMirrorSyncJobPayload(job.Progress)
		if legacyErr == nil {
			payload.SourceID = legacy.SourceID
			payload.Ref = legacy.Ref
			payload.Trigger = legacy.Trigger
			payload.RequestedBy = legacy.RequestedBy
			payload.TraceID = legacy.TraceID
		}
	}
	return SourceMirrorJobDiagnostics{
		Ref:            payload.Ref,
		Trigger:        payload.Trigger,
		RequestedBy:    payload.RequestedBy,
		TraceID:        firstNonEmptyString(payload.TraceID, job.ID),
		FailedStage:    payload.FailedStage,
		ErrorCode:      payload.ErrorCode,
		Retryable:      payload.Retryable,
		GitOperation:   payload.GitOperation,
		GitAttempt:     payload.GitAttempt,
		GitMaxAttempts: payload.GitMaxAttempts,
	}
}

func classifySourceMirrorError(stage string, err error) sourceMirrorErrorDiagnostic {
	message := strings.ToLower(SanitizeKBProgressMessage(errorString(err)))
	stage = strings.TrimSpace(stage)
	containsAny := func(values ...string) bool {
		for _, value := range values {
			if strings.Contains(message, value) {
				return true
			}
		}
		return false
	}

	if stage == KBSourceMirrorStageReconciling || containsAny("reconcile mirrored source repository") {
		return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorReconcile, Retryable: true}
	}
	if containsAny("push source mirror to ags", "remote rejected", "failed to push") {
		retryable := isRetryableGitMirrorError(err) ||
			errors.Is(err, context.DeadlineExceeded) ||
			containsAny("context deadline exceeded", "connection reset", "connection refused", "unexpected eof", "early eof", "status code: 502", "status code: 503", "status code: 504")
		return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorAGSPush, Retryable: retryable}
	}
	if containsAny("lease was lost", "lease lost") {
		return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorLeaseLost, Retryable: true}
	}
	if errors.Is(err, transport.ErrAuthorizationFailed) ||
		errors.Is(err, transport.ErrAuthenticationRequired) ||
		containsAny("authentication failed", "authentication required", "authorization failed", "invalid username or password", "permission denied", "not authorized", "status code: 401", "status code: 403") {
		return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorUpstreamAuth}
	}
	if errors.Is(err, transport.ErrRepositoryNotFound) || containsAny("repository not found", "status code: 404") {
		return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorUpstreamMissing}
	}
	if containsAny("reference not found", "remote ref does not exist", "couldn't find remote ref", "invalid reference") {
		return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorRefMissing}
	}
	if containsAny("proxyconnect", "proxy error", "proxy authentication") {
		return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorProxy, Retryable: true}
	}
	if containsAny("tls handshake", "x509:", "certificate signed", "server name") {
		return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorTLS, Retryable: true}
	}
	var dnsErr *net.DNSError
	if errors.As(err, &dnsErr) || containsAny("no such host", "server misbehaving", "temporary failure in name resolution") {
		return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorDNS, Retryable: true}
	}
	if errors.Is(err, context.DeadlineExceeded) || containsAny("context deadline exceeded", "operation timed out", "connection timed out") {
		return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorCloneTimeout, Retryable: true}
	}
	var netErr net.Error
	if errors.As(err, &netErr) || containsAny("connection reset", "connection refused", "unexpected eof", "early eof", "http/2 stream", "status code: 502", "status code: 503", "status code: 504") {
		return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorNetwork, Retryable: true}
	}
	return sourceMirrorErrorDiagnostic{Code: sourceMirrorErrorInternal, Retryable: false}
}

func errorString(err error) string {
	if err == nil {
		return ""
	}
	return err.Error()
}

func sourceMirrorTrigger(actor string) string {
	if strings.TrimSpace(actor) == "meshx-kb-scheduler" {
		return KBSourceMirrorTriggerScheduled
	}
	return KBSourceMirrorTriggerManual
}

func carrySourceMirrorProgress(raw string, payload sourceMirrorSyncPayload) sourceMirrorSyncPayload {
	current, err := parseSourceMirrorProgress(raw)
	if err != nil {
		return payload
	}
	payload.Trigger = firstNonEmptyString(payload.Trigger, current.Trigger)
	payload.RequestedBy = firstNonEmptyString(payload.RequestedBy, current.RequestedBy)
	payload.TraceID = firstNonEmptyString(payload.TraceID, current.TraceID)
	if !payload.SourceRevisionFenceSet && current.SourceRevisionFenceSet {
		payload.ExpectedCurrentSourceCommitSHA = current.ExpectedCurrentSourceCommitSHA
		payload.SourceRevisionFenceSet = true
	}
	payload.GitOperation = firstNonEmptyString(payload.GitOperation, current.GitOperation)
	if payload.GitAttempt == 0 {
		payload.GitAttempt = current.GitAttempt
	}
	if payload.GitMaxAttempts == 0 {
		payload.GitMaxAttempts = current.GitMaxAttempts
	}
	return payload
}

func (s *KBService) recordSourceMirrorGitProgress(ctx context.Context, job *storekb.Job, leaseOwner string, progress SourceMirrorGitProgress) error {
	payload, err := parseSourceMirrorProgress(job.Progress)
	if err != nil {
		return err
	}
	payload.GitOperation = strings.TrimSpace(progress.Operation)
	payload.GitAttempt = progress.Attempt
	payload.GitMaxAttempts = progress.MaxAttempts
	job.Progress = encodeSourceMirrorProgress(payload)
	job.UpdatedAt = s.now()
	updated, ok, err := s.store.UpdateLeasedJob(ctx, *job, leaseOwner, job.AttemptCount)
	if err != nil {
		return err
	}
	if !ok {
		return conflict("source mirror sync job lease was lost before git progress update")
	}
	*job = updated
	if progress.Retrying {
		diagnostic := classifySourceMirrorError(payload.Stage, progress.Failure)
		s.logSourceMirrorLifecycle(ctx, "retrying", *job, payload, slog.LevelWarn,
			"git_operation", payload.GitOperation,
			"git_attempt", payload.GitAttempt,
			"git_max_attempts", payload.GitMaxAttempts,
			"error_class", diagnostic.Code,
			"retryable", diagnostic.Retryable,
			"error", SanitizeKBProgressMessage(errorString(progress.Failure)),
		)
	}
	return nil
}

func (s *KBService) logSourceMirrorLifecycle(ctx context.Context, event string, job storekb.Job, payload sourceMirrorSyncPayload, level slog.Level, extra ...any) {
	stage := firstNonEmptyString(payload.FailedStage, payload.Stage)
	elapsed := time.Duration(0)
	if job.StartedAt != nil {
		elapsed = s.now().Sub(*job.StartedAt)
	} else if !job.CreatedAt.IsZero() {
		elapsed = s.now().Sub(job.CreatedAt)
	}
	attrs := []any{
		"event", strings.TrimSpace(event),
		"job_id", job.ID,
		"workspace_id", job.WorkspaceID,
		"kb_id", job.KnowledgeBaseID,
		"source_id", firstNonEmptyString(payload.SourceID, job.SourceID),
		"stage", stage,
		"attempt", job.AttemptCount,
		"elapsed_ms", max(elapsed.Milliseconds(), 0),
		"trigger", payload.Trigger,
		"trace_id", firstNonEmptyString(payload.TraceID, job.ID),
	}
	attrs = append(attrs, extra...)
	s.logger.Log(ctx, level, "kb source mirror sync lifecycle", attrs...)
}
