package api

import (
	"fmt"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/gofiber/fiber/v2"

	"fiko-ai/internal/conversation"
	"fiko-ai/internal/event"
	"fiko-ai/internal/protocol"
)

// conversationDTO konuşmanın API görünümüdür; Skill/Tool/Context alanları Phase 1'de gizlenir.
type conversationDTO struct {
	ID        string    `json:"id"`
	Title     string    `json:"title"`
	SessionID string    `json:"sessionId"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

func toConversationDTO(c *conversation.Conversation) conversationDTO {
	return conversationDTO{
		ID: c.ID, Title: c.Title, SessionID: c.SessionID,
		CreatedAt: c.CreatedAt, UpdatedAt: c.UpdatedAt,
	}
}

type conversationsResponse struct {
	Items  []conversationDTO `json:"items"`
	Count  int               `json:"count"`
	Limit  int               `json:"limit"`
	Offset int               `json:"offset"`
}

type createConversationRequest struct {
	Title string `json:"title"`
}

type renameConversationRequest struct {
	Title string `json:"title"`
}

// maxRenameRunes elle verilen başlığın üst sınırı (otomatik başlık 48 karakterdir; elle daha uzun olabilir).
const maxRenameRunes = 120

// listConversations GET /api/conversations?limit=&offset= — kullanıcının konuşmaları,
// UpdatedAt'e göre yeniden eskiye. Yalnızca sahibi kendi konuşmalarını görür.
func (s *Server) listConversations(c *fiber.Ctx) error {
	limit, offset, err := s.parsePaging(c)
	if err != nil {
		return err
	}
	convs, err := s.deps.Conversations.List(c.UserContext(), s.currentUser(c), limit, offset)
	if err != nil {
		return failDomain(c, err)
	}
	items := make([]conversationDTO, 0, len(convs))
	for i := range convs {
		items = append(items, toConversationDTO(&convs[i]))
	}
	return c.JSON(conversationsResponse{Items: items, Count: len(items), Limit: limit, Offset: offset})
}

// createConversation POST /api/conversations — yeni konuşma. Gövde opsiyoneldir.
// Başlık verilmezse konuşma servisi ilk kullanıcı mesajından türetir (bkz. conversation.TitleFrom).
func (s *Server) createConversation(c *fiber.Ctx) error {
	userID := s.currentUser(c)
	var req createConversationRequest
	if len(c.Body()) > 0 {
		if err := c.BodyParser(&req); err != nil {
			return fail(c, fiber.StatusBadRequest, protocol.ErrCodeBadRequest, "geçersiz istek gövdesi")
		}
	}
	conv, err := s.deps.Conversations.Create(c.UserContext(), userID)
	if err != nil {
		return failDomain(c, err)
	}
	if ok, err := s.deps.Conversations.SetTitle(c.UserContext(), conv.ID, req.Title); err != nil {
		return failDomain(c, err)
	} else if ok {
		conv.Title = strings.TrimSpace(req.Title)
	}
	s.publish(event.ConversationCreated, userID, conv.ID, map[string]any{"title": conv.Title})
	return c.Status(fiber.StatusCreated).JSON(conversationResponse{Conversation: toConversationDTO(conv)})
}

// getConversation GET /api/conversations/:id
func (s *Server) getConversation(c *fiber.Ctx) error {
	conv, err := s.deps.Conversations.Get(c.UserContext(), s.currentUser(c), c.Params("id"))
	if err != nil {
		return failDomain(c, err)
	}
	return c.JSON(conversationResponse{Conversation: toConversationDTO(conv)})
}

// renameConversation PUT /api/conversations/:id — başlığı değiştirir. Gövde: {"title": "..."}.
// Sahiplik ÖNCE doğrulanır (yabancı konuşma 404; SetTitle sahipliğe bakmaz).
func (s *Server) renameConversation(c *fiber.Ctx) error {
	userID, convID := s.currentUser(c), strings.Clone(c.Params("id"))
	var req renameConversationRequest
	if err := c.BodyParser(&req); err != nil {
		return fail(c, fiber.StatusBadRequest, protocol.ErrCodeBadRequest, "geçersiz istek gövdesi")
	}
	title := strings.TrimSpace(req.Title)
	if title == "" {
		return fail(c, fiber.StatusBadRequest, protocol.ErrCodeBadRequest, "başlık boş olamaz")
	}
	if utf8.RuneCountInString(title) > maxRenameRunes {
		return fail(c, fiber.StatusBadRequest, protocol.ErrCodeBadRequest,
			fmt.Sprintf("başlık en çok %d karakter olabilir", maxRenameRunes))
	}
	if _, err := s.deps.Conversations.Get(c.UserContext(), userID, convID); err != nil {
		return failDomain(c, err)
	}
	if _, err := s.deps.Conversations.SetTitle(c.UserContext(), convID, title); err != nil {
		return failDomain(c, err)
	}
	conv, err := s.deps.Conversations.Get(c.UserContext(), userID, convID)
	if err != nil {
		return failDomain(c, err)
	}
	s.publish(event.ConversationRenamed, userID, convID, map[string]any{"title": title})
	return c.JSON(conversationResponse{Conversation: toConversationDTO(conv)})
}

// deleteConversation DELETE /api/conversations/:id — konuşma ve tüm mesajları silinir.
func (s *Server) deleteConversation(c *fiber.Ctx) error {
	// Params kopyalanmamış (istek tamponuna işaret eden) string döner; olay veriyolu ASENKRON olduğundan
	// kimlik handler döndükten sonra da okunur. Kopyalanmazsa tampon ezilince olaydaki kimlik bozulur.
	userID, convID := s.currentUser(c), strings.Clone(c.Params("id"))
	if err := s.deps.Conversations.Delete(c.UserContext(), userID, convID); err != nil {
		return failDomain(c, err)
	}
	s.publish(event.ConversationDeleted, userID, convID, nil)
	return c.SendStatus(fiber.StatusNoContent)
}

// listMessages GET /api/conversations/:id/messages?limit= — son N mesaj (eskiden yeniye).
// limit+1 istenir; fazlalık geldiyse hasMore=true döner.
func (s *Server) listMessages(c *fiber.Ctx) error {
	convID := c.Params("id")
	limit, err := s.parseLimit(c, s.cfg.DefaultPageSize)
	if err != nil {
		return err
	}
	msgs, err := s.deps.Conversations.Messages(c.UserContext(), s.currentUser(c), convID, limit+1)
	if err != nil {
		return failDomain(c, err)
	}
	// Depo "son limit mesaj" döndürür; fazlalık en ESKİ mesajdır, sondan kesilir.
	hasMore := len(msgs) > limit
	if hasMore {
		msgs = msgs[len(msgs)-limit:]
	}
	if msgs == nil {
		msgs = []conversation.Message{}
	}
	return c.JSON(messagesResponse{Items: msgs, Count: len(msgs), Limit: limit, HasMore: hasMore})
}

// ---- yardımcılar ----

// publish olay varsa yayınlar (bus yoksa sessizce geçilir: API olay yayınlamak zorunda değildir).
func (s *Server) publish(t event.Type, userID, convID string, data map[string]any) {
	if s.deps.Bus == nil {
		return
	}
	s.deps.Bus.Publish(s.ctx, event.Event{Type: t, UserID: userID, ConversationID: convID, Data: data})
}

// parsePaging limit/offset sorgu parametrelerini doğrular ve varsayılanları uygular.
// Hata *fiber.Error'dur: handler `return err` ile errorHandler'a bırakır (gövdeyi o yazar).
// DİKKAT: burada fail() döndürülmez — fail yazma başarılıysa nil döner, handler devam eder
// ve hata gövdesini normal yanıtla ezerdi.
func (s *Server) parsePaging(c *fiber.Ctx) (limit, offset int, err error) {
	limit, err = s.parseLimit(c, s.cfg.DefaultPageSize)
	if err != nil {
		return 0, 0, err
	}
	offset, err = parseNonNegativeInt(c.Query("offset"), "offset")
	if err != nil {
		return 0, 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	return limit, offset, nil
}

// parseLimit limit parametresini okur: boş/0 → varsayılan, üst sınır → MaxPageSize.
// Hata durumunda *fiber.Error döner (bkz. parsePaging).
func (s *Server) parseLimit(c *fiber.Ctx, def int) (int, error) {
	v, err := parseNonNegativeInt(c.Query("limit"), "limit")
	if err != nil {
		return 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
	}
	switch {
	case v == 0:
		return def, nil
	case v > s.cfg.MaxPageSize:
		return s.cfg.MaxPageSize, nil
	default:
		return v, nil
	}
}

// parseNonNegativeInt sorgu değerini tamsayıya çevirir; boş değer 0 kabul edilir.
func parseNonNegativeInt(raw, name string) (int, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0, nil
	}
	n, err := strconv.Atoi(raw)
	if err != nil {
		return 0, fmt.Errorf("%s sayı olmalı", name)
	}
	if n < 0 {
		return 0, fmt.Errorf("%s negatif olamaz", name)
	}
	return n, nil
}

type conversationResponse struct {
	Conversation conversationDTO `json:"conversation"`
}

type messagesResponse struct {
	Items   []conversation.Message `json:"items"`
	Count   int                    `json:"count"`
	Limit   int                    `json:"limit"`
	HasMore bool                   `json:"hasMore"`
}
