package app

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

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

const giteaConnectionAPIRequestTimeout = 5 * time.Second

type giteaConnectionTargetScope string

const (
	giteaConnectionTargetOrganization giteaConnectionTargetScope = "organization"
	giteaConnectionTargetRepository   giteaConnectionTargetScope = "repository"
)

type giteaRepositoryRef struct {
	Owner              string
	Name               string
	DisplayLabel       string
	ExternalSubjectRef string
}

type giteaConnectionTarget struct {
	Scope              giteaConnectionTargetScope
	DisplayLabel       string
	ExternalSubjectRef string
	Organization       string
	Repository         giteaRepositoryRef
}

type giteaConnectionWebhook struct {
	ID     int64 `json:"id"`
	Config struct {
		URL string `json:"url"`
	} `json:"config"`
}

type giteaUpstreamError struct {
	StatusCode int
	Body       string
}

// ErrGiteaConnectionConfigurationFailed identifies a provider rejection which
// cannot succeed by retrying the same connection operation against the same
// Gitea configuration. The concrete error also unwraps to the underlying
// public error class so synchronous callers keep the existing HTTP contract
// without exposing provider or internal details.
var ErrGiteaConnectionConfigurationFailed = errors.New("gitea connection configuration failed")

type giteaConnectionConfigurationError struct {
	message string
	cause   error
}

func (err giteaConnectionConfigurationError) Error() string {
	return strings.TrimSpace(err.message)
}

func (err giteaConnectionConfigurationError) Unwrap() []error {
	cause := err.cause
	if cause == nil {
		cause = ErrBadRequest
	}
	return []error{ErrGiteaConnectionConfigurationFailed, cause}
}

func (err *giteaUpstreamError) Error() string {
	if err == nil {
		return ""
	}
	return fmt.Sprintf("status=%d body=%s", err.StatusCode, err.Body)
}

func formatGiteaConnectionError(prefix string, err error) error {
	if err == nil {
		return nil
	}
	message := strings.TrimSpace(prefix)
	if message == "" {
		message = "Gitea request failed"
	}
	// Never return provider response bodies, URLs, transport details, or decode
	// errors to clients. Gitea instances can return attacker-controlled HTML or
	// JSON (and intermediaries may include credentials in diagnostics); callers
	// get a fixed operation-level message while the internal error remains
	// available to server-side instrumentation.
	return badRequest(message)
}

func formatGiteaTargetVerificationError(prefix string, err error) error {
	message := strings.TrimSpace(prefix)
	if message == "" {
		message = "Failed to verify Gitea target"
	}
	return formatGiteaApprovalProviderError(message, err)
}

func formatGiteaApprovalProviderError(prefix string, err error) error {
	message := strings.TrimSpace(prefix)
	if message == "" {
		message = "Failed to configure Gitea connection"
	}
	if isDeterministicGiteaProviderRejection(err) {
		return giteaConnectionConfigurationError{message: message, cause: ErrBadRequest}
	}
	return formatGiteaConnectionError(message, err)
}

// Provider transport failures, 5xx responses, and retry-oriented 4xx statuses
// remain transient. A bounded non-retryable 4xx set means an Approval
// delivery can stop retrying only when the provider has deterministically
// rejected the target or current credential/configuration.
func isDeterministicGiteaProviderRejection(err error) bool {
	var upstream *giteaUpstreamError
	if !errors.As(err, &upstream) {
		return false
	}
	if upstream.StatusCode < http.StatusBadRequest || upstream.StatusCode >= http.StatusInternalServerError {
		return false
	}
	switch upstream.StatusCode {
	case http.StatusRequestTimeout, http.StatusConflict, http.StatusTooEarly, http.StatusTooManyRequests:
		return false
	default:
		return true
	}
}

func parseGiteaConnectionTargetInput(value string) (giteaConnectionTarget, error) {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return giteaConnectionTarget{}, fmt.Errorf("target is required")
	}
	switch strings.Count(trimmed, "/") {
	case 0:
		return newGiteaOrganizationTarget(trimmed), nil
	case 1:
		repository, err := parseGiteaRepositoryRefInput(trimmed)
		if err != nil {
			return giteaConnectionTarget{}, err
		}
		return newGiteaRepositoryTarget(repository), nil
	default:
		return giteaConnectionTarget{}, fmt.Errorf("target must be either organization or owner/repo")
	}
}

func parseGiteaRepositoryRefInput(value string) (giteaRepositoryRef, error) {
	parts := strings.Split(strings.TrimSpace(value), "/")
	if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" {
		return giteaRepositoryRef{}, fmt.Errorf("repository must use owner/repo format")
	}
	return newGiteaRepositoryRef(parts[0], parts[1]), nil
}

func newGiteaOrganizationTarget(value string) giteaConnectionTarget {
	label := strings.TrimSpace(value)
	return giteaConnectionTarget{
		Scope:              giteaConnectionTargetOrganization,
		DisplayLabel:       label,
		ExternalSubjectRef: strings.ToLower(label),
		Organization:       label,
	}
}

func newGiteaRepositoryRef(owner string, name string) giteaRepositoryRef {
	trimmedOwner := strings.TrimSpace(owner)
	trimmedName := strings.TrimSpace(name)
	label := trimmedOwner + "/" + trimmedName
	return giteaRepositoryRef{
		Owner:              trimmedOwner,
		Name:               trimmedName,
		DisplayLabel:       label,
		ExternalSubjectRef: strings.ToLower(label),
	}
}

func newGiteaRepositoryTarget(repository giteaRepositoryRef) giteaConnectionTarget {
	return giteaConnectionTarget{
		Scope:              giteaConnectionTargetRepository,
		DisplayLabel:       repository.DisplayLabel,
		ExternalSubjectRef: repository.ExternalSubjectRef,
		Repository:         repository,
	}
}

func (target giteaConnectionTarget) webhookPath() string {
	switch target.Scope {
	case giteaConnectionTargetOrganization:
		return "/orgs/" + url.PathEscape(target.Organization) + "/hooks"
	case giteaConnectionTargetRepository:
		return fmt.Sprintf("/repos/%s/%s/hooks", url.PathEscape(target.Repository.Owner), url.PathEscape(target.Repository.Name))
	default:
		return ""
	}
}

func (target giteaConnectionTarget) webhookPathByID(webhookID int64) string {
	return fmt.Sprintf("%s/%d", target.webhookPath(), webhookID)
}

func (store *Store) verifyGiteaConnectionTarget(ctx context.Context, config provider.GiteaAppConfig, target giteaConnectionTarget) (giteaConnectionTarget, error) {
	switch target.Scope {
	case giteaConnectionTargetOrganization:
		request, cancel, err := store.newGiteaConnectionAPIRequest(ctx, config, http.MethodGet, "/orgs/"+url.PathEscape(target.Organization), nil)
		if err != nil {
			return giteaConnectionTarget{}, err
		}
		defer cancel()
		response, err := store.appHTTPClient.Do(request)
		if err != nil {
			return giteaConnectionTarget{}, fmt.Errorf("verify gitea organization: %w", err)
		}
		defer response.Body.Close()
		if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
			return giteaConnectionTarget{}, readGiteaConnectionUpstreamError(response)
		}
		var resolved struct {
			UserName string `json:"username"`
		}
		if err := json.NewDecoder(response.Body).Decode(&resolved); err != nil {
			return giteaConnectionTarget{}, fmt.Errorf("decode gitea organization: %w", err)
		}
		if strings.TrimSpace(resolved.UserName) != "" {
			return newGiteaOrganizationTarget(resolved.UserName), nil
		}
		return target, nil
	case giteaConnectionTargetRepository:
		path := fmt.Sprintf("/repos/%s/%s", url.PathEscape(target.Repository.Owner), url.PathEscape(target.Repository.Name))
		request, cancel, err := store.newGiteaConnectionAPIRequest(ctx, config, http.MethodGet, path, nil)
		if err != nil {
			return giteaConnectionTarget{}, err
		}
		defer cancel()
		response, err := store.appHTTPClient.Do(request)
		if err != nil {
			return giteaConnectionTarget{}, fmt.Errorf("verify gitea repository: %w", err)
		}
		defer response.Body.Close()
		if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
			return giteaConnectionTarget{}, readGiteaConnectionUpstreamError(response)
		}
		var resolved struct {
			FullName string `json:"full_name"`
			Name     string `json:"name"`
			Owner    struct {
				Login string `json:"login"`
			} `json:"owner"`
		}
		if err := json.NewDecoder(response.Body).Decode(&resolved); err != nil {
			return giteaConnectionTarget{}, fmt.Errorf("decode gitea repository: %w", err)
		}
		if strings.TrimSpace(resolved.FullName) != "" {
			repository, err := parseGiteaRepositoryRefInput(resolved.FullName)
			if err != nil {
				return giteaConnectionTarget{}, err
			}
			return newGiteaRepositoryTarget(repository), nil
		}
		if strings.TrimSpace(resolved.Owner.Login) != "" && strings.TrimSpace(resolved.Name) != "" {
			return newGiteaRepositoryTarget(newGiteaRepositoryRef(resolved.Owner.Login, resolved.Name)), nil
		}
		return target, nil
	default:
		return giteaConnectionTarget{}, fmt.Errorf("unsupported Gitea target")
	}
}

func (store *Store) listGiteaConnectionWebhooks(ctx context.Context, config provider.GiteaAppConfig, target giteaConnectionTarget) ([]giteaConnectionWebhook, error) {
	request, cancel, err := store.newGiteaConnectionAPIRequest(ctx, config, http.MethodGet, target.webhookPath(), nil)
	if err != nil {
		return nil, err
	}
	defer cancel()
	response, err := store.appHTTPClient.Do(request)
	if err != nil {
		return nil, fmt.Errorf("list gitea %s webhooks: %w", target.Scope, err)
	}
	defer response.Body.Close()
	if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
		return nil, readGiteaConnectionUpstreamError(response)
	}
	var hooks []giteaConnectionWebhook
	if err := json.NewDecoder(response.Body).Decode(&hooks); err != nil {
		return nil, fmt.Errorf("decode gitea %s webhooks: %w", target.Scope, err)
	}
	return hooks, nil
}

func (store *Store) createGiteaConnectionWebhook(ctx context.Context, config provider.GiteaAppConfig, target giteaConnectionTarget, webhookSecret string) (int64, error) {
	callbackURL := store.giteaWebhookURLForWorkspace(config.WorkspaceID)
	if callbackURL == "" {
		return 0, fmt.Errorf("AgentTeam public base URL is not configured")
	}
	payload, err := json.Marshal(map[string]any{
		"type": "gitea",
		"config": map[string]any{
			"url":          callbackURL,
			"content_type": "json",
			"secret":       strings.TrimSpace(webhookSecret),
		},
		"events": []string{"pull_request", "pull_request_review", "pull_request_comment", "issue_comment", "push", "status", "workflow_run", "workflow_job"},
		"active": true,
	})
	if err != nil {
		return 0, fmt.Errorf("encode gitea webhook payload: %w", err)
	}
	request, cancel, err := store.newGiteaConnectionAPIRequest(ctx, config, http.MethodPost, target.webhookPath(), payload)
	if err != nil {
		return 0, err
	}
	defer cancel()
	response, err := store.appHTTPClient.Do(request)
	if err != nil {
		return 0, fmt.Errorf("create gitea %s webhook: %w", target.Scope, err)
	}
	defer response.Body.Close()
	if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
		return 0, readGiteaConnectionUpstreamError(response)
	}
	var created giteaConnectionWebhook
	if err := json.NewDecoder(response.Body).Decode(&created); err != nil {
		return 0, fmt.Errorf("decode gitea %s webhook: %w", target.Scope, err)
	}
	if created.ID <= 0 {
		return 0, fmt.Errorf("decode gitea %s webhook: missing id", target.Scope)
	}
	return created.ID, nil
}

func (store *Store) deleteGiteaConnectionWebhookByID(ctx context.Context, config provider.GiteaAppConfig, target giteaConnectionTarget, webhookID int64) error {
	if webhookID <= 0 {
		return nil
	}
	request, cancel, err := store.newGiteaConnectionAPIRequest(ctx, config, http.MethodDelete, target.webhookPathByID(webhookID), nil)
	if err != nil {
		return err
	}
	defer cancel()
	response, err := store.appHTTPClient.Do(request)
	if err != nil {
		return fmt.Errorf("delete gitea %s webhook: %w", target.Scope, err)
	}
	defer response.Body.Close()
	if response.StatusCode == http.StatusNotFound {
		return nil
	}
	if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
		return readGiteaConnectionUpstreamError(response)
	}
	return nil
}

func (store *Store) findAgentTeamGiteaConnectionWebhookIDs(hooks []giteaConnectionWebhook, workspaceID string) []int64 {
	wantURL := store.giteaWebhookURLForWorkspace(workspaceID)
	if wantURL == "" {
		return nil
	}
	ids := make([]int64, 0, len(hooks))
	for _, hook := range hooks {
		if hook.ID > 0 && strings.TrimSpace(hook.Config.URL) == wantURL {
			ids = append(ids, hook.ID)
		}
	}
	return ids
}

func (store *Store) newGiteaConnectionAPIRequest(ctx context.Context, config provider.GiteaAppConfig, method string, path string, payload []byte) (*http.Request, context.CancelFunc, error) {
	requestCtx, cancel := context.WithTimeout(ctx, giteaConnectionAPIRequestTimeout)
	request, err := http.NewRequestWithContext(requestCtx, method, strings.TrimRight(config.InstanceURL, "/")+"/api/v1"+path, bytes.NewReader(payload))
	if err != nil {
		cancel()
		return nil, nil, fmt.Errorf("build gitea api request: %w", err)
	}
	request.Header.Set("Accept", "application/json")
	request.Header.Set("Authorization", "token "+strings.TrimSpace(config.APIToken))
	if len(payload) > 0 {
		request.Header.Set("Content-Type", "application/json")
	}
	return request, cancel, nil
}

func (store *Store) giteaWebhookURLForWorkspace(workspaceID string) string {
	if strings.TrimSpace(store.publicBaseURL) == "" {
		return ""
	}
	baseURL := strings.TrimRight(store.publicBaseURL, "/") + "/api/apps/gitea/webhook"
	if strings.TrimSpace(workspaceID) == "" {
		return baseURL
	}
	return baseURL + "?workspace_id=" + url.QueryEscape(strings.TrimSpace(workspaceID))
}

func readGiteaConnectionUpstreamError(response *http.Response) error {
	if response == nil {
		return nil
	}
	body, _ := io.ReadAll(io.LimitReader(response.Body, 4096))
	return &giteaUpstreamError{StatusCode: response.StatusCode, Body: strings.TrimSpace(string(body))}
}
