package appruntime

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

	"github.com/tidbcloud/AgentTeam/internal/core/webhook"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

const (
	defaultCallbackHTTPTimeout      = 10 * time.Second
	defaultCallbackLeaseDuration    = 2 * time.Minute
	defaultCallbackPollInterval     = 5 * time.Second
	defaultCallbackResponseMaxBytes = 4096
	defaultCallbackCapabilityTTL    = time.Minute

	callbackCapabilityTokenHeader = "X-AgentTeam-Capability-Token"

	CallbackDeliveryResultDelivered      = "delivered"
	CallbackDeliveryResultRetryScheduled = "retry_scheduled"

	CallbackErrorRequestFailed = "callback_request_failed"
	CallbackErrorHTTPStatus    = "callback_http_status"
)

// DeliveryStore is the service actor runtime persistence needed by the App
// callback transport.
type DeliveryStore interface {
	LoadRuntimeCallbackConfig(ctx context.Context, workspaceID string, serviceInstallationID string) (serviceactor.RuntimeCallbackConfig, error)
	ClaimDeliveryEnvelopes(ctx context.Context, params serviceactor.DeliveryDispatchClaim) ([]serviceactor.DeliveryEnvelope, error)
	IssueAppCapabilityToken(ctx context.Context, installationID string, requestID string, scopes []string, ttl time.Duration) (string, error)
	MarkDeliveryAttemptDelivered(ctx context.Context, params serviceactor.DeliveryAttemptDeliveredMark) (serviceactor.DeliveryAttempt, error)
	LoadDeliveryAttempt(ctx context.Context, workspaceID string, attemptID string) (serviceactor.DeliveryAttempt, error)
	ScheduleDeliveryAttemptRetry(ctx context.Context, params serviceactor.DeliveryAttemptRetrySchedule) (serviceactor.DeliveryAttemptRetryResult, error)
}

// CallbackService delivers App service event envelopes to installed callback
// endpoints.
type CallbackService struct {
	store       DeliveryStore
	httpClient  *http.Client
	logger      *slog.Logger
	now         func() time.Time
	baseBackoff time.Duration
	maxBackoff  time.Duration
	lease       time.Duration
	urlPolicy   CallbackURLPolicy
}

// CallbackServiceOptions configures callback transport delivery.
type CallbackServiceOptions struct {
	Store                    DeliveryStore
	HTTPClient               *http.Client
	Logger                   *slog.Logger
	Now                      func() time.Time
	BaseBackoff              time.Duration
	MaxBackoff               time.Duration
	LeaseDuration            time.Duration
	AllowInsecureCallbackURL bool
	ResolveCallbackHostIPs   func(ctx context.Context, host string) ([]net.IP, error)
}

// CallbackURLPolicy constrains callback endpoint URLs before runtime dispatch.
type CallbackURLPolicy struct {
	AllowInsecure  bool
	ResolveHostIPs func(ctx context.Context, host string) ([]net.IP, error)
}

// CallbackDispatchParams selects one callback delivery batch.
type CallbackDispatchParams struct {
	WorkspaceID           string
	ServiceInstallationID string
	SubscriptionKind      string
	Limit                 int
	Now                   time.Time
}

// CallbackDispatchResult summarizes one dispatch batch.
type CallbackDispatchResult struct {
	Attempted int
	Delivered int
	Retried   int
	Results   []CallbackDeliveryResult
}

// CallbackDeliveryResult summarizes one callback attempt result.
type CallbackDeliveryResult struct {
	DeliveryID     string
	AttemptID      string
	Status         string
	ResponseStatus int
	ErrorCode      string
	ErrorDetail    string
}

type callbackEnvelope struct {
	EnvelopeID      string          `json:"envelope_id"`
	DeliveryID      string          `json:"delivery_id"`
	AttemptID       string          `json:"attempt_id"`
	EventType       string          `json:"event_type"`
	EventAction     string          `json:"event_action,omitempty"`
	Payload         json.RawMessage `json:"payload"`
	DeliveryToken   string          `json:"delivery_token"`
	CapabilityToken string          `json:"capability_token"`
	IssuedAt        string          `json:"issued_at"`
}

// NewCallbackService constructs a App callback transport service.
func NewCallbackService(options CallbackServiceOptions) *CallbackService {
	policy := CallbackURLPolicy{
		AllowInsecure:  options.AllowInsecureCallbackURL,
		ResolveHostIPs: options.ResolveCallbackHostIPs,
	}
	client := options.HTTPClient
	if client == nil {
		client = newCallbackHTTPClient(policy)
	}
	return &CallbackService{
		store:       options.Store,
		httpClient:  client,
		logger:      options.Logger,
		now:         options.Now,
		baseBackoff: options.BaseBackoff,
		maxBackoff:  options.MaxBackoff,
		lease:       options.LeaseDuration,
		urlPolicy:   policy,
	}
}

// DispatchCallbackDeliveries claims ready App deliveries for one installation,
// posts signed envelopes to the configured callback endpoint, and records the
// transport result on the delivery attempt.
func (service *CallbackService) DispatchCallbackDeliveries(ctx context.Context, params CallbackDispatchParams) (CallbackDispatchResult, error) {
	if service == nil || service.store == nil {
		return CallbackDispatchResult{}, fmt.Errorf("app callback transport store is not configured")
	}
	workspaceID := strings.TrimSpace(params.WorkspaceID)
	installationID := strings.TrimSpace(params.ServiceInstallationID)
	if workspaceID == "" || installationID == "" {
		return CallbackDispatchResult{}, fmt.Errorf("workspace and installation are required")
	}
	config, err := service.store.LoadRuntimeCallbackConfig(ctx, workspaceID, installationID)
	if err != nil {
		return CallbackDispatchResult{}, err
	}
	callbackURL, err := callbackEventsURL(ctx, config.CallbackURL, service.urlPolicy)
	if err != nil {
		return CallbackDispatchResult{}, err
	}
	secret := strings.TrimSpace(config.CallbackSigningSecret)
	if secret == "" {
		return CallbackDispatchResult{}, fmt.Errorf("app callback signing secret is not configured")
	}
	now := params.Now.UTC()
	if now.IsZero() {
		now = service.nowUTC()
	}
	subscriptionKind, err := callbackSubscriptionKind(params.SubscriptionKind)
	if err != nil {
		return CallbackDispatchResult{}, err
	}
	envelopes, err := service.store.ClaimDeliveryEnvelopes(ctx, serviceactor.DeliveryDispatchClaim{
		WorkspaceID:           workspaceID,
		ServiceInstallationID: installationID,
		SubscriptionKind:      subscriptionKind,
		DeliveryPolicy:        serviceactor.AppRuntimeDeliveryPolicyExternal,
		ServiceConnectionRef:  serviceactor.AppRuntimeConnectionRefExternal,
		TransportKind:         serviceactor.RuntimeTransportKindCallback,
		LeaseOwner:            "callback:" + installationID,
		LeaseDuration:         callbackLeaseDuration(service.lease),
		Limit:                 params.Limit,
		Now:                   now,
		CallbackURLSnapshot:   callbackURL,
	})
	if err != nil {
		return CallbackDispatchResult{}, err
	}

	result := CallbackDispatchResult{Results: make([]CallbackDeliveryResult, 0, len(envelopes))}
	for _, envelope := range envelopes {
		item := service.dispatchOne(ctx, callbackURL, secret, installationID, envelope, now)
		result.Attempted++
		switch item.Status {
		case CallbackDeliveryResultDelivered:
			result.Delivered++
		case CallbackDeliveryResultRetryScheduled:
			result.Retried++
		}
		result.Results = append(result.Results, item)
	}
	return result, nil
}

// RunCallbackDispatcher periodically dispatches callback deliveries until the
// context is canceled.
func (service *CallbackService) RunCallbackDispatcher(ctx context.Context, params CallbackDispatchParams, interval time.Duration) {
	if interval <= 0 {
		interval = defaultCallbackPollInterval
	}
	service.dispatchAndLog(ctx, params)
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			service.dispatchAndLog(ctx, params)
		}
	}
}

func (service *CallbackService) dispatchAndLog(ctx context.Context, params CallbackDispatchParams) {
	result, err := service.DispatchCallbackDeliveries(ctx, params)
	if err != nil {
		if service != nil && service.logger != nil {
			service.logger.WarnContext(ctx, "app callback dispatch failed", "workspace_id", params.WorkspaceID, "service_installation_id", params.ServiceInstallationID, "error", err)
		}
		return
	}
	if service != nil && service.logger != nil && result.Attempted > 0 {
		service.logger.InfoContext(ctx, "app callback dispatch completed", "workspace_id", params.WorkspaceID, "service_installation_id", params.ServiceInstallationID, "attempted", result.Attempted, "delivered", result.Delivered, "retried", result.Retried)
	}
}

func (service *CallbackService) dispatchOne(ctx context.Context, callbackURL string, secret string, installationID string, envelope serviceactor.DeliveryEnvelope, now time.Time) CallbackDeliveryResult {
	item := CallbackDeliveryResult{DeliveryID: envelope.Delivery.ID, AttemptID: envelope.Attempt.ID}
	capabilityToken, err := service.store.IssueAppCapabilityToken(
		ctx,
		installationID,
		callbackCapabilityRequestID(envelope),
		nil,
		defaultCallbackCapabilityTTL,
	)
	if err != nil {
		return service.scheduleRetry(ctx, envelope, now, 0, CallbackErrorRequestFailed, err.Error(), "")
	}
	body, err := encodeCallbackEnvelope(envelope, capabilityToken, now)
	if err != nil {
		return service.scheduleRetry(ctx, envelope, now, 0, CallbackErrorRequestFailed, err.Error(), "")
	}
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, callbackURL, bytes.NewReader(body))
	if err != nil {
		return service.scheduleRetry(ctx, envelope, now, 0, CallbackErrorRequestFailed, err.Error(), "")
	}
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("Authorization", "Bearer "+envelope.DeliveryToken)
	request.Header.Set(callbackCapabilityTokenHeader, capabilityToken)
	signature := webhook.RemoteCallbackSignature(secret, body)
	request.Header.Set("AgentTeam-Signature", signature)
	request.Header.Set("X-AgentTeam-Signature", signature)
	request.Header.Set("AgentTeam-Delivery-ID", envelope.Delivery.ID)
	request.Header.Set("AgentTeam-Attempt-ID", envelope.Attempt.ID)
	request.Header.Set("AgentTeam-Event-Type", envelope.EventType)

	response, err := service.httpClient.Do(request)
	if err != nil {
		return service.scheduleRetry(ctx, envelope, now, 0, CallbackErrorRequestFailed, err.Error(), "")
	}
	defer func() { _ = response.Body.Close() }()
	excerpt := readResponseExcerpt(response.Body)
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		detail := fmt.Sprintf("callback returned HTTP %d", response.StatusCode)
		if excerpt != "" {
			detail += ": " + excerpt
		}
		return service.scheduleRetry(ctx, envelope, now, response.StatusCode, CallbackErrorHTTPStatus, detail, excerpt)
	}
	_, err = service.store.MarkDeliveryAttemptDelivered(ctx, serviceactor.DeliveryAttemptDeliveredMark{
		WorkspaceID:        envelope.Delivery.WorkspaceID,
		AttemptID:          envelope.Attempt.ID,
		ResponseStatusCode: &response.StatusCode,
		ResponseExcerpt:    excerpt,
		Now:                now,
	})
	if err != nil {
		if service.deliveryAttemptAlreadyCompleted(ctx, envelope) {
			item.Status = CallbackDeliveryResultDelivered
			item.ResponseStatus = response.StatusCode
			return item
		}
		return service.scheduleRetry(ctx, envelope, now, response.StatusCode, CallbackErrorRequestFailed, err.Error(), excerpt)
	}
	item.Status = CallbackDeliveryResultDelivered
	item.ResponseStatus = response.StatusCode
	return item
}

func (service *CallbackService) deliveryAttemptAlreadyCompleted(ctx context.Context, envelope serviceactor.DeliveryEnvelope) bool {
	if service == nil || service.store == nil {
		return false
	}
	attempt, err := service.store.LoadDeliveryAttempt(ctx, envelope.Delivery.WorkspaceID, envelope.Attempt.ID)
	if err != nil {
		return false
	}
	switch attempt.Status {
	case serviceactor.DeliveryAttemptStatusDelivered, serviceactor.DeliveryAttemptStatusAcked, serviceactor.DeliveryAttemptStatusFailed:
		return true
	default:
		return false
	}
}

func (service *CallbackService) scheduleRetry(ctx context.Context, envelope serviceactor.DeliveryEnvelope, now time.Time, statusCode int, errorCode string, errorDetail string, excerpt string) CallbackDeliveryResult {
	item := CallbackDeliveryResult{
		DeliveryID:     envelope.Delivery.ID,
		AttemptID:      envelope.Attempt.ID,
		Status:         CallbackDeliveryResultRetryScheduled,
		ResponseStatus: statusCode,
		ErrorCode:      strings.TrimSpace(errorCode),
		ErrorDetail:    strings.TrimSpace(errorDetail),
	}
	var responseStatus *int
	if statusCode > 0 {
		responseStatus = &statusCode
	}
	_, err := service.store.ScheduleDeliveryAttemptRetry(ctx, serviceactor.DeliveryAttemptRetrySchedule{
		WorkspaceID:    envelope.Delivery.WorkspaceID,
		AttemptID:      envelope.Attempt.ID,
		ErrorCode:      item.ErrorCode,
		ErrorDetail:    item.ErrorDetail,
		ResponseStatus: responseStatus,
		ResponseBody:   excerpt,
		BaseBackoff:    service.baseBackoff,
		MaxBackoff:     service.maxBackoff,
		Now:            now,
	})
	if err != nil {
		item.ErrorCode = CallbackErrorRequestFailed
		item.ErrorDetail = strings.TrimSpace(err.Error())
	}
	return item
}

func encodeCallbackEnvelope(envelope serviceactor.DeliveryEnvelope, capabilityToken string, now time.Time) ([]byte, error) {
	payload := json.RawMessage(bytes.TrimSpace(envelope.EventPayload))
	if len(payload) == 0 {
		payload = json.RawMessage(`{}`)
	}
	if strings.TrimSpace(capabilityToken) == "" {
		return nil, fmt.Errorf("callback capability token is required")
	}
	encoded, err := json.Marshal(callbackEnvelope{
		EnvelopeID:      envelope.EnvelopeID,
		DeliveryID:      envelope.Delivery.ID,
		AttemptID:       envelope.Attempt.ID,
		EventType:       envelope.EventType,
		EventAction:     envelope.EventAction,
		Payload:         payload,
		DeliveryToken:   envelope.DeliveryToken,
		CapabilityToken: capabilityToken,
		IssuedAt:        now.UTC().Format(time.RFC3339Nano),
	})
	if err != nil {
		return nil, fmt.Errorf("encode callback envelope: %w", err)
	}
	return encoded, nil
}

func callbackCapabilityRequestID(envelope serviceactor.DeliveryEnvelope) string {
	return "app-callback:" + strings.TrimSpace(envelope.Delivery.ID) + ":" + strings.TrimSpace(envelope.Attempt.ID)
}

func callbackSubscriptionKind(value string) (string, error) {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return serviceactor.SubscriptionKindAppMention, nil
	}
	if !serviceactor.IsAppRuntimeSubscriptionKind(trimmed) {
		return "", fmt.Errorf("app callback subscription kind %q is not supported", trimmed)
	}
	return trimmed, nil
}

func callbackEventsURL(ctx context.Context, callbackURL string, policy CallbackURLPolicy) (string, error) {
	trimmed := strings.TrimSpace(callbackURL)
	if trimmed == "" {
		return "", fmt.Errorf("app callback URL is not configured")
	}
	parsed, err := url.Parse(trimmed)
	if err != nil {
		return "", fmt.Errorf("app callback URL is invalid: %w", err)
	}
	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return "", fmt.Errorf("app callback URL must use http or https")
	}
	if parsed.Hostname() == "" {
		return "", fmt.Errorf("app callback URL host is required")
	}
	if parsed.User != nil {
		return "", fmt.Errorf("app callback URL must not include userinfo")
	}
	if parsed.RawQuery != "" || parsed.Fragment != "" {
		return "", fmt.Errorf("app callback URL must not include query or fragment")
	}
	if !policy.AllowInsecure {
		if parsed.Scheme != "https" {
			return "", fmt.Errorf("app callback URL must use https")
		}
		if port := parsed.Port(); port != "" && port != "443" {
			return "", fmt.Errorf("app callback URL must use the default https port")
		}
		if err := validatePublicCallbackHost(ctx, parsed.Hostname(), policy.ResolveHostIPs); err != nil {
			return "", err
		}
	}
	parsed.Path = strings.TrimRight(parsed.Path, "/") + "/events"
	parsed.RawPath = ""
	return parsed.String(), nil
}

func validatePublicCallbackHost(ctx context.Context, host string, resolver func(context.Context, string) ([]net.IP, error)) error {
	_, err := publicCallbackHostIPs(ctx, host, resolver)
	return err
}

func newCallbackHTTPClient(policy CallbackURLPolicy) *http.Client {
	transport := http.DefaultTransport.(*http.Transport).Clone()
	transport.Proxy = nil
	dialer := &net.Dialer{Timeout: defaultCallbackHTTPTimeout, KeepAlive: defaultCallbackHTTPTimeout}
	transport.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
		if policy.AllowInsecure {
			return dialer.DialContext(ctx, network, address)
		}
		host, port, err := net.SplitHostPort(address)
		if err != nil {
			return nil, fmt.Errorf("parse app callback dial target: %w", err)
		}
		ips, err := publicCallbackHostIPs(ctx, host, policy.ResolveHostIPs)
		if err != nil {
			return nil, err
		}
		var lastErr error
		for _, ip := range ips {
			conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
			if err == nil {
				return conn, nil
			}
			lastErr = err
		}
		if lastErr != nil {
			return nil, lastErr
		}
		return nil, fmt.Errorf("app callback URL host has no IP addresses")
	}
	return &http.Client{
		Timeout:   defaultCallbackHTTPTimeout,
		Transport: transport,
		CheckRedirect: func(*http.Request, []*http.Request) error {
			return fmt.Errorf("app callback redirects are disabled")
		},
	}
}

func publicCallbackHostIPs(ctx context.Context, host string, resolver func(context.Context, string) ([]net.IP, error)) ([]net.IP, error) {
	trimmed := strings.TrimSpace(strings.TrimSuffix(host, "."))
	if trimmed == "" {
		return nil, fmt.Errorf("app callback URL host is required")
	}
	lower := strings.ToLower(trimmed)
	if lower == "localhost" || strings.HasSuffix(lower, ".localhost") {
		return nil, fmt.Errorf("app callback URL host must be publicly routable")
	}
	if ip := net.ParseIP(trimmed); ip != nil {
		if !callbackIPIsPublic(ip) {
			return nil, fmt.Errorf("app callback URL host must be publicly routable")
		}
		return []net.IP{ip}, nil
	}
	if resolver == nil {
		resolver = defaultCallbackHostResolver
	}
	ips, err := resolver(ctx, trimmed)
	if err != nil {
		return nil, fmt.Errorf("resolve app callback URL host: %w", err)
	}
	if len(ips) == 0 {
		return nil, fmt.Errorf("app callback URL host has no IP addresses")
	}
	publicIPs := make([]net.IP, 0, len(ips))
	for _, ip := range ips {
		if ip == nil {
			return nil, fmt.Errorf("app callback URL host must resolve to public IP addresses")
		}
		if !callbackIPIsPublic(ip) {
			return nil, fmt.Errorf("app callback URL host must resolve to public IP addresses")
		}
		publicIPs = append(publicIPs, ip)
	}
	return publicIPs, nil
}

func defaultCallbackHostResolver(ctx context.Context, host string) ([]net.IP, error) {
	addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
	if err != nil {
		return nil, err
	}
	ips := make([]net.IP, 0, len(addrs))
	for _, addr := range addrs {
		if addr.IP != nil {
			ips = append(ips, addr.IP)
		}
	}
	return ips, nil
}

func callbackIPIsPublic(ip net.IP) bool {
	if ip == nil {
		return false
	}
	if ipv4 := ip.To4(); ipv4 != nil {
		ip = ipv4
	}
	if !ip.IsGlobalUnicast() || ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() || ip.IsMulticast() {
		return false
	}
	for _, blocked := range callbackBlockedIPNets {
		if blocked.Contains(ip) {
			return false
		}
	}
	return true
}

var callbackBlockedIPNets = mustParseCallbackIPNets([]string{
	"0.0.0.0/8",
	"10.0.0.0/8",
	"100.64.0.0/10",
	"127.0.0.0/8",
	"169.254.0.0/16",
	"172.16.0.0/12",
	"192.0.0.0/24",
	"192.0.2.0/24",
	"192.168.0.0/16",
	"198.18.0.0/15",
	"198.51.100.0/24",
	"203.0.113.0/24",
	"224.0.0.0/4",
	"240.0.0.0/4",
	"255.255.255.255/32",
	"::/128",
	"::1/128",
	"64:ff9b::/96",
	"100::/64",
	"2001:db8::/32",
	"fc00::/7",
	"fe80::/10",
	"ff00::/8",
})

func mustParseCallbackIPNets(values []string) []*net.IPNet {
	result := make([]*net.IPNet, 0, len(values))
	for _, value := range values {
		_, network, err := net.ParseCIDR(value)
		if err != nil {
			panic(err)
		}
		result = append(result, network)
	}
	return result
}

func readResponseExcerpt(body io.Reader) string {
	if body == nil {
		return ""
	}
	data, _ := io.ReadAll(io.LimitReader(body, defaultCallbackResponseMaxBytes+1))
	if len(data) > defaultCallbackResponseMaxBytes {
		data = data[:defaultCallbackResponseMaxBytes]
	}
	return strings.TrimSpace(string(data))
}

func callbackLeaseDuration(value time.Duration) time.Duration {
	if value <= 0 {
		return defaultCallbackLeaseDuration
	}
	return value
}

func (service *CallbackService) nowUTC() time.Time {
	if service != nil && service.now != nil {
		return service.now().UTC()
	}
	return time.Now().UTC()
}
