// Package errs defines the application's error type (AppError) and helpers
// to map domain error codes to HTTP status codes. Service layers wrap
// underlying errors with errs.Wrap / errs.New; transport layers use errs.As
// + errs.HTTPStatus to translate them into Google API style responses
// ({code, message, fields, request_id}).
package errs

import (
	"errors"
	"fmt"
	"net/http"
)

// Code is a stable, machine-readable identifier for a class of error.
// Codes are part of the public API contract surfaced by HTTP responses.
type Code string

// Canonical error codes. Keep this list in sync with HTTPStatus and any
// downstream consumers (e.g., HTTP transport, OpenAPI schemas).
const (
	CodeInvalidInput       Code = "invalid_input"
	CodeUnauthorized       Code = "unauthorized"
	CodeForbidden          Code = "forbidden"
	CodeNotFound           Code = "not_found"
	CodeConflict           Code = "conflict"
	CodeFailedPrecondition Code = "failed_precondition"
	CodeRateLimited        Code = "rate_limited"
	CodeUpstream           Code = "upstream_error"
	CodeUnavailable        Code = "unavailable"
	CodeInternal           Code = "internal"

	// Project 模块专用细粒度 code（HTTP 仍 409，但前端可据此区分提示）。
	CodeProjectSlugTaken  Code = "project_slug_taken"
	CodeProjectArchived   Code = "project_archived"
	CodeRequirementClosed Code = "requirement_closed"
	CodePhaseGateFailed   Code = "phase_gate_failed"

	// Workspace 模块细粒度 code
	CodeWorkspaceSlugTaken      Code = "workspace_slug_taken"
	CodeWorkspaceCloneFailed    Code = "workspace_clone_failed"
	CodeWorkspaceSyncInProgress Code = "workspace_sync_in_progress"
	CodeWorkspaceRemoteInvalid  Code = "workspace_remote_invalid"
	CodeWorktreeBranchConflict  Code = "worktree_branch_conflict"
	CodeWorktreeAlreadyActive   Code = "worktree_already_active"
	CodeWorktreeNotHeldByCaller Code = "worktree_not_held_by_caller"
	CodeWorktreePathInvalid     Code = "worktree_path_invalid"
	CodeGitCredentialInvalid    Code = "git_credential_invalid"
	CodeGitCmdFailed            Code = "git_cmd_failed"

	// Chat 模块
	CodeChatConversationNotFound      Code = "chat.conversation_not_found"
	CodeChatMessageNotFound           Code = "chat.message_not_found"
	CodeChatMessageNotPending         Code = "chat.message_not_pending"
	CodeChatConcurrentMessage         Code = "chat.concurrent_message"
	CodeChatAttachmentTooLarge        Code = "chat.attachment_too_large"
	CodeChatAttachmentUnsupportedMime Code = "chat.attachment_unsupported_mime"
	CodeChatModelCapabilityMismatch   Code = "chat.model_capability_mismatch"
	CodeChatTemplateNotFound          Code = "chat.template_not_found"
	CodeChatEndpointNotFound          Code = "chat.endpoint_not_found"
	CodeChatModelNotFound             Code = "chat.model_not_found"

	// LLM 域
	CodeLLMUpstream       Code = "llm.upstream_error"
	CodeLLMTimeout        Code = "llm.timeout"
	CodeLLMRateLimited    Code = "llm.rate_limited"
	CodeLLMContextTooLong Code = "llm.context_too_long"

	// Storage 域
	CodeStoragePutFailed Code = "storage.put_failed"
	CodeStorageGetFailed Code = "storage.get_failed"

	// Jobs 域
	CodeJobNotFound          Code = "job.not_found"
	CodeJobInvalidType       Code = "job.invalid_type"
	CodeJobPayloadInvalid    Code = "job.payload_invalid"
	CodeJobPayloadTooLarge   Code = "job.payload_too_large"
	CodeWebhookNotConfigured Code = "webhook.not_configured"

	// ACP 域
	CodeAcpAgentKindNotFound        Code = "acp.agent_kind_not_found"
	CodeAcpAgentKindDisabled        Code = "acp.agent_kind_disabled"
	CodeAcpAgentKindInUse           Code = "acp.agent_kind_in_use"
	CodeAcpSessionNotFound          Code = "acp.session_not_found"
	CodeAcpSessionQuotaExceeded     Code = "acp.session_quota_exceeded"
	CodeAcpSessionNotOwned          Code = "acp.session_not_owned"
	CodeAcpSessionTerminated        Code = "acp.session_terminated"
	CodeAcpSessionBranchConflict    Code = "acp.session_branch_conflict"
	CodeAcpSpawnFailed              Code = "acp.spawn_failed"
	CodeAcpHandshakeTimeout         Code = "acp.handshake_timeout"
	CodeAcpFsPathOutsideWorktree    Code = "acp.fs_path_outside_worktree"
	CodeAcpPermissionNotFound       Code = "acp.permission_not_found"
	CodeAcpPermissionAlreadyDecided Code = "acp.permission_already_decided"
	CodeAcpPermissionInvalidOption  Code = "acp.permission_invalid_option"

	// MCP 域
	CodeMcpTokenInvalid         Code = "mcp.token_invalid"
	CodeMcpTokenRevoked         Code = "mcp.token_revoked"
	CodeMcpTokenExpired         Code = "mcp.token_expired"
	CodeMcpTokenNotFound        Code = "mcp.token_not_found"
	CodeMcpTokenScopeViolation  Code = "mcp.token_scope_violation"
	CodeMcpTokenNameRequired    Code = "mcp.token_name_required"
	CodeMcpTokenExpiresRequired Code = "mcp.token_expires_required"
	CodeMcpRateLimited          Code = "mcp.rate_limited"

	// Run 域（workspace run profiles）
	CodeRunProfileNotFound       Code = "run.profile_not_found"
	CodeRunProfileSlugTaken      Code = "run.profile_slug_taken"
	CodeRunProfileAlreadyRunning Code = "run.profile_already_running"
	CodeRunProfileNotRunning     Code = "run.profile_not_running"
	CodeRunProfileDisabled       Code = "run.profile_disabled"
	CodeRunSpawnFailed           Code = "run.spawn_failed"
	CodeRunCommandInvalid        Code = "run.command_invalid"
	// 一次性 exec（run_command / run_tests）专用错误。
	CodeRunExecTimeout           Code = "run.exec_timeout"
	CodeRunExecCommandNotAllowed Code = "run.exec_command_not_allowed"
	CodeRunExecFailed            Code = "run.exec_failed"
)

// AppError is the application's structured error type. It carries a stable
// Code, a human-readable Message, an optional underlying Cause, and an
// optional Fields map for per-field validation details.
type AppError struct {
	Code    Code
	Message string
	Cause   error
	Fields  map[string]any
}

// Error implements the error interface. When a Cause is present the format
// is "<message>: <cause>"; otherwise just the message is returned.
func (e *AppError) Error() string {
	if e.Cause != nil {
		return fmt.Sprintf("%s: %s", e.Message, e.Cause.Error())
	}
	return e.Message
}

// Unwrap exposes the underlying cause so errors.Is / errors.As can traverse
// the chain.
func (e *AppError) Unwrap() error { return e.Cause }

// WithFields attaches per-field validation details and returns the receiver
// for chaining.
func (e *AppError) WithFields(f map[string]any) *AppError {
	e.Fields = f
	return e
}

// New constructs a new AppError with the given code and message.
func New(code Code, msg string) *AppError {
	return &AppError{Code: code, Message: msg}
}

// Wrap wraps an existing error with an AppError carrying the given code and
// message. The original error is preserved as Cause and remains discoverable
// via errors.Is / errors.As.
func Wrap(cause error, code Code, msg string) *AppError {
	return &AppError{Code: code, Message: msg, Cause: cause}
}

// As is a convenience wrapper around errors.As that extracts the first
// *AppError in err's chain. It returns (nil, false) when err is nil or no
// AppError is found.
func As(err error) (*AppError, bool) {
	if err == nil {
		return nil, false
	}
	var ae *AppError
	if errors.As(err, &ae) {
		return ae, true
	}
	return nil, false
}

// HTTPStatus maps an error Code to its canonical HTTP status code. Unknown
// codes default to 500 Internal Server Error.
func HTTPStatus(code Code) int {
	switch code {
	case CodeInvalidInput:
		return http.StatusBadRequest
	case CodeUnauthorized:
		return http.StatusUnauthorized
	case CodeForbidden:
		return http.StatusForbidden
	case CodeNotFound:
		return http.StatusNotFound
	case CodeConflict,
		CodeProjectSlugTaken, CodeProjectArchived, CodeRequirementClosed, CodePhaseGateFailed,
		CodeWorkspaceSlugTaken, CodeWorkspaceSyncInProgress,
		CodeWorktreeBranchConflict, CodeWorktreeAlreadyActive:
		return http.StatusConflict
	case CodeFailedPrecondition:
		return http.StatusPreconditionFailed
	case CodeRateLimited:
		return http.StatusTooManyRequests
	case CodeUpstream:
		return http.StatusBadGateway
	case CodeUnavailable:
		return http.StatusServiceUnavailable
	case CodeWorkspaceCloneFailed, CodeGitCmdFailed:
		return http.StatusBadGateway
	case CodeWorkspaceRemoteInvalid, CodeWorktreePathInvalid, CodeGitCredentialInvalid:
		return http.StatusBadRequest
	case CodeWorktreeNotHeldByCaller:
		return http.StatusForbidden
	case CodeChatConversationNotFound, CodeChatMessageNotFound,
		CodeChatTemplateNotFound, CodeChatEndpointNotFound, CodeChatModelNotFound:
		return http.StatusNotFound
	case CodeChatMessageNotPending, CodeChatConcurrentMessage,
		CodeChatModelCapabilityMismatch:
		return http.StatusConflict
	case CodeChatAttachmentTooLarge:
		return http.StatusRequestEntityTooLarge
	case CodeChatAttachmentUnsupportedMime:
		return http.StatusUnsupportedMediaType
	case CodeLLMRateLimited:
		return http.StatusTooManyRequests
	case CodeLLMTimeout:
		return http.StatusGatewayTimeout
	case CodeLLMUpstream, CodeStoragePutFailed, CodeStorageGetFailed:
		return http.StatusBadGateway
	case CodeLLMContextTooLong:
		return http.StatusBadRequest
	case CodeJobNotFound:
		return http.StatusNotFound
	case CodeJobInvalidType, CodeJobPayloadInvalid, CodeJobPayloadTooLarge:
		return http.StatusBadRequest
	case CodeAcpAgentKindNotFound, CodeAcpSessionNotFound:
		return http.StatusNotFound
	case CodeAcpAgentKindDisabled, CodeAcpAgentKindInUse,
		CodeAcpSessionTerminated, CodeAcpSessionBranchConflict:
		return http.StatusConflict
	case CodeAcpSessionQuotaExceeded:
		return http.StatusTooManyRequests
	case CodeAcpSessionNotOwned:
		return http.StatusForbidden
	case CodeAcpSpawnFailed, CodeAcpHandshakeTimeout:
		return http.StatusBadGateway
	case CodeAcpFsPathOutsideWorktree:
		return http.StatusInternalServerError
	case CodeAcpPermissionNotFound:
		return http.StatusNotFound
	case CodeAcpPermissionAlreadyDecided:
		return http.StatusConflict
	case CodeAcpPermissionInvalidOption:
		return http.StatusBadRequest
	case CodeWebhookNotConfigured:
		return http.StatusServiceUnavailable
	case CodeMcpTokenInvalid, CodeMcpTokenRevoked, CodeMcpTokenExpired:
		return http.StatusUnauthorized
	case CodeMcpTokenNotFound:
		return http.StatusNotFound
	case CodeMcpTokenScopeViolation:
		return http.StatusForbidden
	case CodeMcpTokenNameRequired, CodeMcpTokenExpiresRequired:
		return http.StatusBadRequest
	case CodeMcpRateLimited:
		return http.StatusTooManyRequests
	case CodeRunProfileNotFound:
		return http.StatusNotFound
	case CodeRunProfileSlugTaken, CodeRunProfileAlreadyRunning,
		CodeRunProfileNotRunning, CodeRunProfileDisabled:
		return http.StatusConflict
	case CodeRunCommandInvalid:
		return http.StatusBadRequest
	case CodeRunSpawnFailed:
		return http.StatusBadGateway
	case CodeRunExecTimeout:
		return http.StatusGatewayTimeout
	case CodeRunExecCommandNotAllowed:
		return http.StatusForbidden
	case CodeRunExecFailed:
		return http.StatusInternalServerError
	default:
		return http.StatusInternalServerError
	}
}
