package diagnostics

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"sort"
	"strings"
)

type Code string

const (
	CodeMissingCredential          Code = "missing_credential"
	CodeConfigCredential           Code = "config_credential"
	CodeLiveAuthFailure            Code = "live_auth_failure"
	CodeLiveTransportFailure       Code = "live_transport_failure"
	CodeLiveAPIFailure             Code = "live_api_failure"
	CodeConfigurationError         Code = "configuration_error"
	CodeInvalidAPIBaseURL          Code = "invalid_api_base_url"
	CodeUnsupportedMockPayload     Code = "unsupported_mock_payload"
	CodeFixtureReadOnly            Code = "fixture_read_only"
	CodeFixtureFallbackDetected    Code = "fixture_fallback_detected"
	CodeUnsupportedCapability      Code = "unsupported_capability"
	CodeCacheBusy                  Code = "cache_busy"
	CodeCacheSchemaBlocked         Code = "cache_schema_blocked"
	CodeSchemaDecode               Code = "schema_decode"
	CodeAPIFailure                 Code = "api_validation"
	CodePushMirrorSyncInProgress   Code = "push_mirror_sync_in_progress"
	CodeInvalidQuery               Code = "invalid_query"
	CodePRReviewAnchorMismatch     Code = "pr_review_anchor_mismatch"
	CodeWriteConfirmationMissing   Code = "write_confirmation_incomplete"
	CodeDiscussionReplyUnavailable Code = "discussion_reply_unavailable"
	CodeParentPRNotCached          Code = "parent_pr_not_cached"
	CodeWriteAmbiguousRemote       Code = "write_ambiguous_remote"
	CodeWriteAmbiguousReadback     Code = "write_ambiguous_readback_failed"
	CodeWriteConflict              Code = "write_conflict"
	CodeOperationCancelled         Code = "operation_cancelled"
	CodeOperationTimeout           Code = "operation_timeout"
)

type Diagnostic struct {
	Code          Code              `json:"failure_class"`
	Message       string            `json:"message"`
	ExitClass     string            `json:"exit_class"`
	HTTPAttempted bool              `json:"http_attempted"`
	Retryable     bool              `json:"retryable"`
	Context       map[string]string `json:"context,omitempty"`
}

type CommandContext struct {
	ProviderMode                string
	Command                     string
	SelectedAPIBaseURL          string
	RepositoryBindingID         string
	CachePathPresent            bool
	AuditPathPresent            bool
	CacheReadiness              string
	CacheSchemaDetected         int
	CacheSchemaExpected         int
	PayloadKind                 string
	PayloadSource               string
	RequiredField               string
	HTTPStatus                  int
	HTTPAttempted               bool
	SensitiveTerms              []string
	SensitiveContextKeys        []string
	BroaderConfigurationInvalid bool
	MissingCredential           bool
	InvalidSelectedAPIBaseURL   bool
	UnsupportedPayload          bool
	FixtureFallbackSentinel     bool
	FixtureReadOnly             bool
	TransportFailure            bool
	APIFailure                  bool
	SchemaDecodeFailure         bool
	MalformedSuccess            bool
	LocalPayloadTooLarge        bool
	FailureSource               string
	RetryableProviderFailure    bool
}

type Classifier struct {
	Filter Filter
}

func NewClassifier(sensitiveTerms ...string) Classifier {
	return Classifier{Filter: NewFilter(sensitiveTerms...)}
}

func Classify(err error, ctx CommandContext) Diagnostic {
	return NewClassifier(ctx.SensitiveTerms...).Classify(err, ctx)
}

func (c Classifier) Classify(err error, ctx CommandContext) Diagnostic {
	filter := c.Filter
	if len(ctx.SensitiveTerms) > 0 {
		filter.SensitiveTerms = append(filter.SensitiveTerms, ctx.SensitiveTerms...)
	}
	attempted, known := HTTPAttemptEvidence(err)
	if known {
		ctx.HTTPAttempted = attempted
	}
	code := classifyCode(err, ctx)
	d := Diagnostic{Code: code, Message: messageFor(code, err), ExitClass: exitClassFor(code), HTTPAttempted: httpAttemptedFor(code, ctx), Retryable: retryableFor(code) || ctx.RetryableProviderFailure}
	if known {
		d.HTTPAttempted = attempted
	}
	d.Message = filter.RedactText(d.Message)
	d.Context = redactedContext(filter, ctx)
	return d
}

func (d Diagnostic) Error() string {
	if d.Message == "" {
		return string(d.Code)
	}
	return d.Message
}

func (d Diagnostic) JSON() ([]byte, error) {
	return json.Marshal(d)
}

func classifyCode(err error, ctx CommandContext) Code {
	if hasCode(err, "cache_schema_blocked") {
		return CodeCacheSchemaBlocked
	}
	if hasCode(err, "pr_review_anchor_mismatch") {
		return CodePRReviewAnchorMismatch
	}
	if hasCode(err, "write_confirmation_incomplete") {
		return CodeWriteConfirmationMissing
	}
	if hasCode(err, "write_ambiguous_remote") {
		return CodeWriteAmbiguousRemote
	}
	if hasCode(err, "write_ambiguous_readback_failed") {
		return CodeWriteAmbiguousReadback
	}
	if hasCode(err, "write_conflict") {
		return CodeWriteConflict
	}
	if hasCode(err, "invalid_query") {
		return CodeInvalidQuery
	}
	if hasCode(err, "discussion_reply_unavailable") {
		return CodeDiscussionReplyUnavailable
	}
	if hasCode(err, "parent_pr_not_cached") {
		return CodeParentPRNotCached
	}
	if ctx.ProviderMode != "live-http" {
		if ctx.FixtureReadOnly || hasCode(err, "fixture_read_only") || hasCode(err, "write_fixture_read_only") {
			return CodeFixtureReadOnly
		}
		return codeFromError(err)
	}
	if ctx.BroaderConfigurationInvalid || hasCode(err, "configuration_error") || (!ctx.HTTPAttempted && isConfigurationInputBug(err)) {
		return CodeConfigCredential
	}
	if ctx.MissingCredential || hasCode(err, "missing_credential") || hasCode(err, "write_missing_credential") {
		return CodeConfigCredential
	}
	if ctx.InvalidSelectedAPIBaseURL || hasCode(err, "invalid_api_base_url") {
		return CodeConfigCredential
	}
	if attempted, known := HTTPAttemptEvidence(err); known && !attempted {
		if code := operationInterruptionCode(err); code != "" {
			return code
		}
	}
	if hasCode(err, "push_mirror_sync_in_progress") && ctx.HTTPAttempted {
		return CodePushMirrorSyncInProgress
	}
	if (ctx.HTTPStatus == http.StatusUnauthorized || ctx.HTTPStatus == http.StatusForbidden || hasCode(err, "live_auth_failure") || hasCode(err, "auth_expired") || hasCode(err, "forbidden") || hasCode(err, "write_unauthorized")) && ctx.HTTPAttempted {
		return CodeAPIFailure
	}
	if hasCode(err, "auth_expired") || hasCode(err, "forbidden") || hasCode(err, "write_unauthorized") || hasCode(err, "live_auth_failure") {
		return CodeConfigCredential
	}
	if ctx.HTTPStatus >= 200 && ctx.HTTPStatus <= 299 && (ctx.SchemaDecodeFailure || ctx.MalformedSuccess || hasCode(err, "schema_decode") || hasCode(err, "malformed_response") || hasCode(err, "unexpected_content_type")) {
		return CodeSchemaDecode
	}
	if ctx.HTTPStatus >= 400 && ctx.HTTPStatus <= 499 && ctx.HTTPStatus != http.StatusUnauthorized && ctx.HTTPStatus != http.StatusForbidden && ctx.HTTPAttempted {
		return CodeAPIFailure
	}
	if ctx.FailureSource == "remote_status" && ctx.HTTPAttempted {
		return CodeAPIFailure
	}
	if ctx.LocalPayloadTooLarge || ctx.FailureSource == "local_body_limit" || ctx.FailureSource == "local_decode_boundary" || ctx.FailureSource == "partial_response" || hasCode(err, "partial_response") || hasCode(err, "unsupported_mock_payload") || hasCode(err, "live_graph_invalid") || hasCode(err, "validation_failed") {
		return CodeSchemaDecode
	}
	if (ctx.APIFailure || hasCode(err, "live_api_failure") || hasCode(err, "write_provider_error")) && ctx.HTTPAttempted {
		return CodeAPIFailure
	}
	if (hasCode(err, "api_validation") || hasCode(err, "not_found") || hasCode(err, "remote_conflict") || hasCode(err, "remote_collision") || hasCode(err, "remote_not_found") || hasCode(err, "rate_limited")) && ctx.HTTPAttempted {
		return CodeAPIFailure
	}
	if ctx.UnsupportedPayload || hasCode(err, "unsupported_mock_payload") || hasCode(err, "live_graph_invalid") || hasCode(err, "validation_failed") {
		return CodeSchemaDecode
	}
	if hasCode(err, "schema_decode") || hasCode(err, "malformed_response") || hasCode(err, "unexpected_content_type") {
		return CodeSchemaDecode
	}
	// Earlier successful HTTP must not turn a later pre-dispatch interruption
	// into a transport failure. Preserve confirmed provider/schema failures
	// above, and any actually entered failed request in a mixed aggregate.
	if attempted, known := HTTPTransportFailureEvidence(err); known && !attempted {
		if code := operationInterruptionCode(err); code != "" {
			return code
		}
	}
	if (ctx.TransportFailure || hasCode(err, "live_transport_failure") || hasCode(err, "network_unavailable") || hasCode(err, "write_network_unavailable")) && ctx.HTTPAttempted {
		return CodeLiveTransportFailure
	}
	if ctx.HTTPStatus >= 500 && ctx.HTTPAttempted {
		return CodeLiveTransportFailure
	}
	// Preserve confirmed provider/schema failures in mixed partial results.
	// A later local interruption is not itself an HTTP transport failure.
	if attempted, known := HTTPAttemptEvidence(err); known && attempted {
		if code := operationInterruptionCode(err); code != "" {
			return code
		}
	}
	if ctx.FixtureFallbackSentinel || hasCode(err, "fixture_fallback_detected") || hasCode(err, "write_fixture_fallback_detected") || hasCode(err, "fixture_read_only") || hasCode(err, "write_fixture_read_only") {
		return CodeFixtureFallbackDetected
	}
	if hasCode(err, "unsupported_capability") {
		return CodeUnsupportedCapability
	}
	if hasCode(err, "cache_busy") {
		return CodeCacheBusy
	}
	return CodeConfigurationError
}

func operationInterruptionCode(err error) Code {
	var interrupted interface{ OperationInterruption() string }
	interruption := ""
	if errors.As(err, &interrupted) {
		interruption = interrupted.OperationInterruption()
	}
	if errors.Is(err, context.Canceled) || interruption == "cancelled" {
		return CodeOperationCancelled
	}
	if errors.Is(err, context.DeadlineExceeded) || interruption == "timeout" {
		return CodeOperationTimeout
	}
	return ""
}

func codeFromError(err error) Code {
	for _, code := range diagnosticCodes(err) {
		switch code {
		case "missing_credential":
			return CodeMissingCredential
		case "live_auth_failure":
			return CodeAPIFailure
		case "live_transport_failure", "network_unavailable", "write_network_unavailable":
			return CodeLiveTransportFailure
		case "live_api_failure", "write_provider_error":
			return CodeAPIFailure
		case "configuration_error":
			return CodeConfigurationError
		case "invalid_api_base_url":
			return CodeInvalidAPIBaseURL
		case "unsupported_mock_payload", "live_graph_invalid", "validation_failed":
			return CodeSchemaDecode
		case "fixture_read_only", "write_fixture_read_only":
			return CodeFixtureReadOnly
		case "fixture_fallback_detected", "write_fixture_fallback_detected":
			return CodeFixtureFallbackDetected
		case "unsupported_capability":
			return CodeUnsupportedCapability
		case "cache_busy":
			return CodeCacheBusy
		case "cache_schema_blocked":
			return CodeCacheSchemaBlocked
		case "schema_decode", "partial_response", "malformed_response", "unexpected_content_type":
			return CodeSchemaDecode
		case "auth_expired", "forbidden", "write_unauthorized", "not_found", "remote_conflict", "remote_collision", "remote_not_found", "rate_limited":
			return CodeAPIFailure
		case "push_mirror_sync_in_progress":
			return CodePushMirrorSyncInProgress
		case "invalid_query":
			return CodeInvalidQuery
		case "pr_review_anchor_mismatch":
			return CodePRReviewAnchorMismatch
		case "write_confirmation_incomplete":
			return CodeWriteConfirmationMissing
		case "discussion_reply_unavailable":
			return CodeDiscussionReplyUnavailable
		case "parent_pr_not_cached":
			return CodeParentPRNotCached
		case "write_ambiguous_remote":
			return CodeWriteAmbiguousRemote
		case "write_ambiguous_readback_failed":
			return CodeWriteAmbiguousReadback
		case "write_conflict":
			return CodeWriteConflict
		}
	}
	return CodeConfigurationError
}

func hasCode(err error, want string) bool {
	for _, code := range diagnosticCodes(err) {
		if code == want {
			return true
		}
	}
	return false
}

func diagnosticCodes(err error) []string {
	var out []string
	var walk func(error)
	walk = func(err error) {
		if err == nil {
			return
		}
		if coded, ok := err.(interface{ DiagnosticCode() string }); ok {
			if code := strings.TrimSpace(coded.DiagnosticCode()); code != "" {
				out = append(out, code)
			}
		}
		switch unwrapped := err.(type) {
		case interface{ Unwrap() []error }:
			for _, child := range unwrapped.Unwrap() {
				walk(child)
			}
		case interface{ Unwrap() error }:
			walk(unwrapped.Unwrap())
		}
	}
	walk(err)
	return out
}

func isConfigurationInputBug(err error) bool {
	if err == nil {
		return false
	}
	for _, code := range diagnosticCodes(err) {
		if code == "invalid_query" || code == "repo_required" || code == "not_found" {
			return true
		}
	}
	return false
}

func httpAttemptedFor(code Code, ctx CommandContext) bool {
	switch code {
	case CodeLiveAuthFailure, CodeLiveTransportFailure, CodeLiveAPIFailure, CodeAPIFailure, CodeSchemaDecode, CodePushMirrorSyncInProgress, CodePRReviewAnchorMismatch, CodeWriteConfirmationMissing, CodeDiscussionReplyUnavailable, CodeWriteAmbiguousRemote, CodeWriteAmbiguousReadback, CodeWriteConflict:
		return ctx.HTTPAttempted
	case CodeConfigCredential:
		return false
	default:
		return false
	}
}

func retryableFor(code Code) bool {
	return code == CodeLiveTransportFailure || code == CodeCacheBusy || code == CodePushMirrorSyncInProgress || code == CodeWriteAmbiguousRemote || code == CodeWriteAmbiguousReadback
}

func exitClassFor(code Code) string {
	switch code {
	case CodeMissingCredential, CodeLiveAuthFailure:
		return "auth"
	case CodeConfigCredential:
		return "configuration"
	case CodeLiveTransportFailure:
		return "transport"
	case CodeOperationCancelled, CodeOperationTimeout:
		return "operation"
	case CodeLiveAPIFailure, CodeAPIFailure, CodePushMirrorSyncInProgress:
		return "provider"
	case CodePRReviewAnchorMismatch, CodeWriteConfirmationMissing, CodeWriteAmbiguousRemote, CodeWriteAmbiguousReadback, CodeWriteConflict:
		return "provider"
	case CodeInvalidQuery:
		return "input"
	case CodeDiscussionReplyUnavailable:
		return "capability"
	case CodeParentPRNotCached:
		return "cache"
	case CodeUnsupportedMockPayload:
		return "payload"
	case CodeFixtureFallbackDetected, CodeFixtureReadOnly:
		return "fixture"
	case CodeUnsupportedCapability:
		return "capability"
	case CodeSchemaDecode:
		return "schema"
	case CodeCacheBusy, CodeCacheSchemaBlocked:
		return "cache"
	default:
		return "configuration"
	}
}

func messageFor(code Code, err error) string {
	base := string(code)
	switch code {
	case CodeMissingCredential:
		base += ": live provider requires a configured credential"
	case CodeConfigCredential:
		base += ": live provider requires valid configuration and credential"
	case CodeLiveAuthFailure:
		base += ": live provider rejected credentials"
	case CodeLiveTransportFailure:
		base += ": live provider request failed before a valid API response"
	case CodeOperationCancelled:
		base += ": operation cancelled"
	case CodeOperationTimeout:
		base += ": operation deadline expired"
	case CodeLiveAPIFailure:
		base += ": live provider returned an API failure"
	case CodeAPIFailure:
		base += ": live provider returned an API validation failure"
	case CodeConfigurationError:
		base += ": live command configuration is invalid"
	case CodeInvalidAPIBaseURL:
		base += ": selected live api_base_url is invalid"
	case CodeUnsupportedMockPayload:
		base += ": live provider payload is unsupported"
	case CodeFixtureReadOnly:
		base += ": fixture provider is read-only"
	case CodeFixtureFallbackDetected:
		base += ": live route reached fixture behavior"
	case CodeUnsupportedCapability:
		base += ": requested capability is not supported for this provider"
	case CodeSchemaDecode:
		base += ": response schema decode failure"
	case CodeCacheBusy:
		base += ": cache is busy, retry later"
	case CodeCacheSchemaBlocked:
		base += ": selected local cache schema blocks this command; live credential/provider readiness is separate from cache schema readiness"
	case CodePushMirrorSyncInProgress:
		base += ": mirror synchronization is already running or cooling down"
	case CodeInvalidQuery:
		base += ": command arguments are invalid"
	case CodePRReviewAnchorMismatch:
		base += ": GitCode created the review comment on a different path or line"
	case CodeWriteConfirmationMissing:
		base += ": remote write anchor could not be safely confirmed"
	case CodeDiscussionReplyUnavailable:
		base += ": provider discussion identity is unavailable for a safe reply"
	case CodeParentPRNotCached:
		base += ": inline review write requires its parent pull request in the selected cache"
	case CodeWriteAmbiguousRemote:
		base += ": remote write outcome is ambiguous; retry the same idempotency key for GET-only recovery"
	case CodeWriteAmbiguousReadback:
		base += ": canonical GET-only recovery could not complete"
	case CodeWriteConflict:
		base += ": canonical issue state changed across a safely rejected update"
	}
	if err != nil {
		return base + ": " + err.Error()
	}
	return base
}

func redactedContext(filter Filter, ctx CommandContext) map[string]string {
	values := map[string]string{}
	put := func(key, value string) {
		if strings.TrimSpace(value) != "" {
			values[key] = filter.RedactText(value)
		}
	}
	put("provider_mode", ctx.ProviderMode)
	put("command", ctx.Command)
	put("api_base_url", filter.RedactURL(ctx.SelectedAPIBaseURL))
	put("repository_binding", ctx.RepositoryBindingID)
	put("payload_kind", ctx.PayloadKind)
	put("payload_source", ctx.PayloadSource)
	put("required_field", ctx.RequiredField)
	if ctx.HTTPStatus != 0 {
		put("http_status", fmt.Sprintf("%d", ctx.HTTPStatus))
	}
	put("cache_path_present", fmt.Sprintf("%t", ctx.CachePathPresent))
	put("audit_path_present", fmt.Sprintf("%t", ctx.AuditPathPresent))
	put("cache_readiness", ctx.CacheReadiness)
	if ctx.CacheSchemaDetected != 0 {
		put("cache_schema_detected", fmt.Sprintf("%d", ctx.CacheSchemaDetected))
	}
	if ctx.CacheSchemaExpected != 0 {
		put("cache_schema_expected", fmt.Sprintf("%d", ctx.CacheSchemaExpected))
	}
	for _, key := range ctx.SensitiveContextKeys {
		delete(values, key)
	}
	if len(values) == 0 {
		return nil
	}
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	out := make(map[string]string, len(values))
	for _, key := range keys {
		out[key] = values[key]
	}
	return out
}