package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/sirupsen/logrus"
)

// ChatServiceAPI allows mocking ChatService in other places
// Only public methods should be included

type ChatServiceAPI interface {
	HandleChat(c *gin.Context)
}

// ChatService handles chat interactions and orchestrates LLM and DB calls.
type ChatService struct {
	LLM      LLMClientAPI
	visitsDB VisitDBAPI
	repo     ChatRepositoryAPI
}

var _ ChatServiceAPI = (*ChatService)(nil)

func NewChatService(llm LLMClientAPI, db VisitDBAPI, repo ChatRepositoryAPI) ChatServiceAPI {
	return &ChatService{LLM: llm, visitsDB: db, repo: repo}
}

// HandleChat is the main entrypoint for chat requests. It delegates to modular helpers.
func (cs *ChatService) HandleChat(c *gin.Context) {
	corrID := uuid.New().String()
	ctx := context.WithValue(context.Background(), correlationIDCtxKey, corrID)
	c.Header("X-Correlation-ID", corrID)
	req, err := cs.parseRequest(c)
	if err != nil {
		return
	}
	kwResp, err := cs.LLM.ExtractKeywords(ctx, req.Message)
	if err != nil {
		cs.persistInteraction(ctx, corrID, req.Message, nil, nil, ChatResponse{Match: nil})
		cs.respondWithError(c, req, nil, err)
		return
	}
	keywords := cs.keywordsToStrings(kwResp["keyword"])
	best, _, err := cs.findBestVisit(ctx, req, keywords)
	resp := cs.buildResponse(best)
	c.JSON(http.StatusOK, resp)
	cs.persistInteraction(ctx, corrID, req.Message, kwResp, best, resp)
}

// parseRequest parses and validates the incoming chat request.
func (cs *ChatService) parseRequest(c *gin.Context) (ChatRequest, error) {
	var req ChatRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		logrus.WithError(err).Error("Invalid request")
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
		return req, err
	}
	return req, nil
}

// extractKeywords gets keywords from the LLM and normalizes them.
func (cs *ChatService) extractKeywords(ctx context.Context, message string) ([]string, error) {
	kwResp, err := cs.LLM.ExtractKeywords(ctx, message)
	if err != nil {
		return nil, err
	}
	return cs.keywordsToStrings(kwResp["keyword"]), nil
}

// findBestVisit finds candidate visits and disambiguates the best match.
func (cs *ChatService) findBestVisit(ctx context.Context, req ChatRequest, keywords []string) (*Visit, string, error) {
	cs.logKeywords(keywords, req.Message)
	candidates, err := cs.visitsDB.FindCandidates(keywords)
	cs.logCandidates(candidates, err)
	if err != nil {
		return nil, "", err
	}
	bestID := ""
	rawDis := ""
	if len(candidates) > 0 {
		if real, ok := cs.LLM.(*LLMClient); ok {
			raw, vr, derr := real.DisambiguateBestMatchRaw(ctx, req.Message, candidates)
			rawDis = raw
			bestID = vr
			if derr != nil {
				cs.logBestID(bestID, derr)
			} else {
				cs.logBestID(bestID, nil)
			}
		} else {
			bestID, err = cs.LLM.DisambiguateBestMatch(ctx, req.Message, candidates)
			cs.logBestID(bestID, err)
		}
	}
	visit, err := cs.visitsDB.FindById(bestID)
	if err != nil {
		return nil, rawDis, fmt.Errorf("FindById: %w", err)
	}
	return &visit, rawDis, nil
}

// buildResponse constructs the ChatResponse from the best Visit.
func (cs *ChatService) buildResponse(best *Visit) ChatResponse {
	if best == nil {
		resp := ChatResponse{Match: nil}
		logrus.WithFields(logrus.Fields{"response": resp}).Info("Build response: no match")
		return resp
	}
	totalPrice, totalDuration := sumProcedures(best.Procedures)
	resp := ChatResponse{
		Match:         &best.ID,
		Procedures:    best.Procedures,
		TotalPrice:    totalPrice,
		TotalDuration: totalDuration,
		Notes:         best.Notes,
	}
	logrus.WithFields(logrus.Fields{"response": resp}).Info("Build response: match found")
	return resp
}

// respondWithError logs and responds with error details.
func (cs *ChatService) respondWithError(c *gin.Context, req ChatRequest, keywords []string, err error) {
	kwMap := map[string]interface{}{"keyword": keywords}
	cs.logChat(req, kwMap, nil, "", err)
	c.JSON(http.StatusOK, ChatResponse{Match: nil})
}

// keywordsToStrings normalizes keyword interface to []string.
func (cs *ChatService) keywordsToStrings(kwIface interface{}) []string {
	var kwArr []string
	switch v := kwIface.(type) {
	case []interface{}:
		for _, item := range v {
			if s, ok := item.(string); ok {
				kwArr = append(kwArr, s)
			}
		}
	case []string:
		kwArr = v
	}
	return kwArr
}

// logKeywords logs extracted keywords.
func (cs *ChatService) logKeywords(keywords []string, message string) {
	logrus.WithFields(logrus.Fields{
		"keywords": keywords,
		"message":  message,
	}).Info("Finding visit candidates")
}

// logCandidates logs candidate visits.
func (cs *ChatService) logCandidates(candidates []Visit, err error) {
	logrus.WithFields(logrus.Fields{
		"candidates": candidates,
		"error":      err,
	}).Info("Candidate visits found")
}

// logBestID logs the best candidate ID.
func (cs *ChatService) logBestID(bestID string, err error) {
	logrus.WithFields(logrus.Fields{
		"bestID": bestID,
		"error":  err,
	}).Info("Disambiguated best match")
}

// logChat logs the chat request and result details.
func (cs *ChatService) logChat(req ChatRequest, keywords interface{}, candidates []Visit, bestID string, err error) {
	var kwMap map[string]interface{}
	switch v := keywords.(type) {
	case []string:
		kwMap = map[string]interface{}{"keyword": v}
	case map[string]interface{}:
		kwMap = v
	default:
		kwMap = map[string]interface{}{"keyword": v}
	}
	logRequest(req, kwMap, candidates, bestID, err)
	if candidates != nil && bestID != "" {
		var best *Visit
		for i := range candidates {
			if candidates[i].ID == bestID {
				best = &candidates[i]
				break
			}
		}
		if best != nil {
			totalPrice, totalDuration := sumProcedures(best.Procedures)
			logrus.WithFields(logrus.Fields{
				"match":          best.ID,
				"total_price":    totalPrice,
				"total_duration": totalDuration,
				"notes":          best.Notes,
			}).Info("Responding with match")
		}
	}
}

// persistInteraction saves chat interaction to repository (best effort)
func (cs *ChatService) persistInteraction(ctx context.Context, correlationID string, userMsg string, kwResp map[string]interface{}, best *Visit, resp ChatResponse) {
	if cs.repo == nil {
		return
	}
	var translate, animal string
	var keywords []string
	if kwResp != nil {
		if t, ok := kwResp["translate"].(string); ok {
			translate = t
		}
		if a, ok := kwResp["animal"].(string); ok {
			animal = a
		}
		keywords = cs.keywordsToStrings(kwResp["keyword"])
	}
	bestID := ""
	if best != nil {
		bestID = best.ID
	}
	rec := ChatInteraction{
		CorrelationID: correlationID,
		UserMessage:   userMsg,
		Translate:     translate,
		Animal:        animal,
		Keywords:      keywords,
		BestVisitID:   bestID,
		TotalPrice:    resp.TotalPrice,
		TotalDuration: resp.TotalDuration,
	}
	if err := cs.repo.SaveChatInteraction(ctx, rec); err != nil {
		logrus.WithError(err).Debug("failed to save chat interaction")
	}
}
