// Package fakegateway provides the deterministic Gateway fixture used by the
// timeline E2E harness. It exercises the real protobuf and HTTP callback boundary.
package fakegateway

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync"
	"time"

	gatewayapi "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/agent-gateway/gateway/v1"
	gatewayv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/agent-gateway/gateway/v1"
	sessionv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/agent-gateway/session/v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

type Server struct {
	gatewayapi.UnimplementedGatewayServiceServer
	CallbackURL string
	Client      *http.Client
	OnDelivery  func(error)
	mu          sync.Mutex
	tasks       map[string]*sessionv1.Task
}

func (s *Server) ProcessMessage(_ context.Context, req *gatewayapi.ProcessMessageRequest) (*gatewayapi.ProcessMessageResponse, error) {
	if req.GetMessage().GetSessionKey() == "" || req.GetMessage().GetMessageId() == "" || req.GetContext().GetInstanceId() == "" {
		return nil, status.Error(codes.InvalidArgument, "message identity and instance ID are required")
	}
	content, err := timelineAnswer(req.GetMessage().GetContent())
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}
	key := req.Context.InstanceId + "/" + req.Message.SessionKey
	sessionHash := sha256.Sum256([]byte(key))
	taskHash := sha256.Sum256([]byte(key + "/" + req.Message.MessageId))
	task := &sessionv1.Task{TaskId: hex.EncodeToString(taskHash[:16]), SessionId: hex.EncodeToString(sessionHash[:16]), Status: sessionv1.TaskStatus_TASK_STATUS_RECEIVED, AgentModel: req.Context.AgentModel}
	s.mu.Lock()
	if s.tasks == nil {
		s.tasks = make(map[string]*sessionv1.Task)
	}
	if previous := s.tasks[task.TaskId]; previous != nil {
		copy := proto.Clone(previous).(*sessionv1.Task)
		s.mu.Unlock()
		return &gatewayapi.ProcessMessageResponse{Disposition: gatewayapi.ProcessMessageDisposition_PROCESS_MESSAGE_DISPOSITION_DUPLICATED, Task: copy}, nil
	}
	s.tasks[task.TaskId] = proto.Clone(task).(*sessionv1.Task)
	s.mu.Unlock()
	event := &gatewayv1.TaskEvent{
		Type: gatewayv1.TaskEventType_TASK_EVENT_TYPE_FINISHED, TaskId: task.TaskId, SessionId: task.SessionId,
		Status: sessionv1.TaskStatus_TASK_STATUS_SUCCEEDED, Content: content,
		TaskSource:    &sessionv1.TaskSource{SourceMessageId: req.Message.MessageId, SenderEmail: req.Message.SenderEmail},
		SessionSource: &sessionv1.SessionSource{Source: &sessionv1.MessageSourceRef{Type: sessionv1.MessageSourceType_MESSAGE_SOURCE_TYPE_APPLICATION, InstanceId: req.Context.InstanceId}, SourceSessionKey: req.Message.SessionKey},
	}
	// May reach the caller before ProcessMessage returns, just like production.
	go func() {
		err := s.deliver(event)
		if s.OnDelivery != nil {
			s.OnDelivery(err)
		}
	}()
	return &gatewayapi.ProcessMessageResponse{Disposition: gatewayapi.ProcessMessageDisposition_PROCESS_MESSAGE_DISPOSITION_ACCEPTED, Task: task}, nil
}

func (s *Server) deliver(event *gatewayv1.TaskEvent) error {
	payload, err := protojson.Marshal(event)
	if err != nil {
		return err
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.CallbackURL, bytes.NewReader(payload))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	client := s.Client
	if client == nil {
		client = http.DefaultClient
	}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	_, _ = io.Copy(io.Discard, resp.Body)
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("callback returned HTTP %d", resp.StatusCode)
	}
	return nil
}

func timelineAnswer(prompt string) (string, error) {
	_, raw, found := strings.Cut(prompt, "Input JSON:\n")
	if !found {
		return "", fmt.Errorf("fake Gateway only supports timeline synthesis prompts")
	}
	var input struct {
		Evidence []struct {
			ID         uint64 `json:"id"`
			OccurredAt string `json:"occurred_at"`
		} `json:"new_evidence"`
	}
	if err := json.Unmarshal([]byte(raw), &input); err != nil {
		return "", err
	}
	entries := []map[string]any{}
	if len(input.Evidence) > 0 {
		ids := make([]uint64, 0, len(input.Evidence))
		for _, evidence := range input.Evidence {
			ids = append(ids, evidence.ID)
		}
		entries = append(entries, map[string]any{
			"occurred_at": input.Evidence[0].OccurredAt, "title": "Gateway fixture: workspace task updated",
			"details":    "Task evidence was synthesized through the Agent Gateway callback.",
			"event_kind": "task_update", "topic": "workspace task", "entities": []string{}, "evidence_ids": ids,
			"draft_fingerprint": fmt.Sprintf("gateway-fixture-task-%d", ids[0]), "confidence": 1,
		})
	}
	answer, err := json.Marshal(map[string]any{"summary": "Workspace task evidence was updated.", "entries": entries})
	return string(answer), err
}
