package app

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

type FeedbackCandidate struct {
	ID                int64   `json:"id"`
	SignalKey         string  `json:"signalKey"`
	Source            string  `json:"source"`
	SignalCode        string  `json:"signalCode"`
	IssueKey          string  `json:"issueKey"`
	JobID             *int64  `json:"jobId"`
	RunDBID           *int64  `json:"runDbId"`
	AnswerFingerprint *string `json:"answerFingerprint"`
	FeedbackID        *int64  `json:"feedbackId"`
	FeedbackAction    *string `json:"feedbackAction"`
	Reason            *string `json:"reason"`
	CreatedAt         string  `json:"createdAt"`
}

type FeedbackSource interface {
	LatestID(context.Context) (int64, error)
	List(context.Context, int64, int) ([]FeedbackCandidate, error)
}

type HTTPFeedbackSource struct {
	baseURL string
	client  *http.Client
}

func NewHTTPFeedbackSource(baseURL string) *HTTPFeedbackSource {
	return &HTTPFeedbackSource{baseURL: strings.TrimRight(baseURL, "/"), client: &http.Client{Transport: http.DefaultTransport}}
}

func (s *HTTPFeedbackSource) LatestID(ctx context.Context) (int64, error) {
	var response struct {
		LatestID int64 `json:"latestId"`
	}
	if err := s.get(ctx, url.Values{"mode": {"latest"}}, &response); err != nil {
		return 0, err
	}
	if response.LatestID < 0 {
		return 0, fmt.Errorf("latest candidate id is negative")
	}
	return response.LatestID, nil
}

func (s *HTTPFeedbackSource) List(ctx context.Context, afterID int64, limit int) ([]FeedbackCandidate, error) {
	var response struct {
		Candidates []FeedbackCandidate `json:"candidates"`
	}
	query := url.Values{}
	query.Set("afterId", fmt.Sprintf("%d", afterID))
	query.Set("limit", fmt.Sprintf("%d", limit))
	if err := s.get(ctx, query, &response); err != nil {
		return nil, err
	}
	previous := afterID
	for index := range response.Candidates {
		candidate := &response.Candidates[index]
		candidate.IssueKey = strings.ToUpper(strings.TrimSpace(candidate.IssueKey))
		feedbackAction := strings.TrimSpace(stringValue(candidate.FeedbackAction))
		validSource := candidate.Source == "feedback" || candidate.Source == "human_better" || candidate.Source == "runtime"
		validFeedback := candidate.Source != "feedback" || (int64Value(candidate.FeedbackID) > 0 && (feedbackAction == "wrong" || feedbackAction == "revise"))
		if candidate.ID <= previous || !issueKeyPattern.MatchString(candidate.IssueKey) || !validSource || !validFeedback ||
			strings.TrimSpace(candidate.SignalKey) == "" || strings.TrimSpace(candidate.SignalCode) == "" ||
			int64Value(candidate.JobID) < 1 || strings.TrimSpace(stringValue(candidate.AnswerFingerprint)) == "" {
			return nil, fmt.Errorf("invalid or unordered single-ticket optimizer candidate %d", candidate.ID)
		}
		previous = candidate.ID
	}
	return response.Candidates, nil
}

func (s *HTTPFeedbackSource) get(ctx context.Context, query url.Values, target any) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.baseURL+"/v1/auto-analysis/optimizer-candidates?"+query.Encode(), nil)
	if err != nil {
		return err
	}
	resp, err := s.client.Do(req)
	if err != nil {
		return fmt.Errorf("request optimizer candidates: %w", err)
	}
	defer resp.Body.Close()
	payload, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024+1))
	if err != nil {
		return err
	}
	if len(payload) > 1024*1024 {
		return fmt.Errorf("optimizer candidate response exceeds 1 MiB")
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("optimizer candidate status %d: %s", resp.StatusCode, truncate(string(payload), 1000))
	}
	if err := json.Unmarshal(payload, target); err != nil {
		return fmt.Errorf("decode optimizer candidate response: %w", err)
	}
	return nil
}

type FeedbackPoller struct {
	store     *Store
	worker    *Worker
	source    FeedbackSource
	interval  time.Duration
	batchSize int
}

func NewFeedbackPoller(store *Store, worker *Worker, source FeedbackSource, interval time.Duration, batchSize int) *FeedbackPoller {
	return &FeedbackPoller{store: store, worker: worker, source: source, interval: interval, batchSize: batchSize}
}

func (p *FeedbackPoller) Start(ctx context.Context) {
	go func() {
		p.pollAndLog(ctx)
		ticker := time.NewTicker(p.interval)
		defer ticker.Stop()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				p.pollAndLog(ctx)
			}
		}
	}()
}

func (p *FeedbackPoller) pollAndLog(ctx context.Context) {
	if err := p.pollOnce(ctx); err != nil && ctx.Err() == nil {
		slog.Error("poll single-ticket optimizer candidates", "error", err)
	}
}

func (p *FeedbackPoller) pollOnce(ctx context.Context) error {
	cursor, initialized := p.store.CandidateCursor()
	if !initialized {
		latest, err := p.source.LatestID(ctx)
		if err != nil {
			return err
		}
		if err := p.store.SetCandidateCursor(latest); err != nil {
			return err
		}
		slog.Info("initialized single-ticket optimizer candidate cursor", "candidate_id", latest)
		return nil
	}
	candidates, err := p.source.List(ctx, cursor, p.batchSize)
	if err != nil {
		return err
	}
	for _, candidate := range candidates {
		request := feedbackNomination(candidate)
		trigger := JobTriggerCandidate
		if candidate.Source == "feedback" {
			trigger = JobTriggerFeedback
		}
		_, _, err := p.worker.Enqueue(Job{
			MessageID:         fmt.Sprintf("auto-candidate:%d", candidate.ID),
			Request:           request,
			IssueKey:          strings.ToUpper(strings.TrimSpace(candidate.IssueKey)),
			Trigger:           trigger,
			CandidateID:       candidate.ID,
			CandidateSource:   candidate.Source,
			CandidateSignal:   candidate.SignalCode,
			SourceRunDBID:     int64Value(candidate.RunDBID),
			FeedbackID:        int64Value(candidate.FeedbackID),
			SourceJobID:       int64Value(candidate.JobID),
			FeedbackAction:    stringValue(candidate.FeedbackAction),
			AnswerFingerprint: stringValue(candidate.AnswerFingerprint),
			Mode:              JobModeTicket,
		})
		if err != nil {
			return fmt.Errorf("enqueue single-ticket candidate %d: %w", candidate.ID, err)
		}
		if err := p.store.SetCandidateCursor(candidate.ID); err != nil {
			return err
		}
	}
	return nil
}

func feedbackNomination(candidate FeedbackCandidate) string {
	reason := strings.TrimSpace(stringValue(candidate.Reason))
	if reason == "" {
		reason = "No written reason was supplied; independently inspect the persisted feedback target and full replay chain."
	}
	fingerprint := strings.TrimSpace(stringValue(candidate.AnswerFingerprint))
	if fingerprint == "" {
		fingerprint = "unavailable"
	}
	if candidate.Source == "feedback" {
		return fmt.Sprintf("Persisted /ao %s feedback (feedback id %d, candidate id %d, answer fingerprint %s): %s", stringValue(candidate.FeedbackAction), int64Value(candidate.FeedbackID), candidate.ID, fingerprint, reason)
	}
	return fmt.Sprintf("Automatic %s signal %s (candidate id %d, answer fingerprint %s): %s", candidate.Source, candidate.SignalCode, candidate.ID, fingerprint, reason)
}

func stringValue(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func int64Value(value *int64) int64 {
	if value == nil {
		return 0
	}
	return *value
}
