package api

import (
	"errors"
	"io"
	"strconv"
	"strings"

	"github.com/gofiber/fiber/v2"

	"fiko-ai/internal/knowledge"
	"fiko-ai/internal/llm"
	"fiko-ai/internal/protocol"
)

// Bilgi Tabanı (RAG, Faz 8) uçları. Belgeler kullanıcıya aittir: her uç s.currentUser(c) ile
// çalışır, başkasının belgesi 404'tür (varlığı bile sızdırılmaz).

type knowledgeDocResponse struct {
	Document knowledge.Document `json:"document"`
}

type knowledgeDocsResponse struct {
	Items  []knowledge.Document `json:"items"`
	Count  int                  `json:"count"`
	Limit  int                  `json:"limit"`
	Offset int                  `json:"offset"`
}

type knowledgeChunksResponse struct {
	Items  []knowledge.Chunk `json:"items"`
	Count  int               `json:"count"`
	Limit  int               `json:"limit"`
	Offset int               `json:"offset"`
}

// knowledgeIngestRequest JSON ile belge ekleme gövdesidir (dosya yüklemek için multipart kullanılır).
type knowledgeIngestRequest struct {
	Title     string   `json:"title"`
	Text      string   `json:"text"`
	Source    string   `json:"source"`
	ProjectID string   `json:"projectId"`
	Tags      []string `json:"tags"`
}

type knowledgeSearchRequest struct {
	Query          string               `json:"query"`
	Mode           knowledge.SearchMode `json:"mode"`     // semantic | keyword | hybrid (boş = hybrid)
	Limit          int                  `json:"limit"`    // boş = 5, en fazla 50
	MinScore       float64              `json:"minScore"` // 0..1
	ProjectID      string               `json:"projectId"`
	DocumentIDs    []string             `json:"documentIds"`
	Tags           []string             `json:"tags"`
	MaxPerDocument int                  `json:"maxPerDocument"` // 0 = varsayılan (2); <0 = sınırsız
}

type knowledgeSearchResponse struct {
	Items []knowledge.Hit      `json:"items"` // skora göre azalan
	Count int                  `json:"count"`
	Mode  knowledge.SearchMode `json:"mode"`
}

// knowledgeAvailable Bilgi Tabanı yapılandırılmamışsa tüm /api/knowledge uçlarını 503 yapar.
func (s *Server) knowledgeAvailable(c *fiber.Ctx) error {
	if s.deps.Knowledge == nil {
		return fail(c, fiber.StatusServiceUnavailable, protocol.ErrCodeInternal, "bilgi tabanı yapılandırılmamış")
	}
	return c.Next()
}

// listKnowledgeDocs GET /api/knowledge/documents?projectId=&status=&tag=&q=&limit=&offset=
func (s *Server) listKnowledgeDocs(c *fiber.Ctx) error {
	limit, offset, err := s.parsePaging(c)
	if err != nil {
		return err
	}
	status := knowledge.Status(strings.TrimSpace(c.Query("status")))
	if status != "" && !status.Valid() {
		return fail(c, fiber.StatusBadRequest, protocol.ErrCodeBadRequest, "status indexing veya ready olmalı")
	}
	f := knowledge.DocumentFilter{
		UserID:    s.currentUser(c),
		ProjectID: strings.Clone(strings.TrimSpace(c.Query("projectId"))),
		Status:    status,
		Tag:       strings.Clone(c.Query("tag")),
		Query:     strings.Clone(c.Query("q")),
	}
	items, err := s.deps.Knowledge.List(c.UserContext(), f, limit, offset)
	if err != nil {
		return failKnowledge(c, err)
	}
	return c.JSON(knowledgeDocsResponse{Items: items, Count: len(items), Limit: limit, Offset: offset})
}

// getKnowledgeDoc GET /api/knowledge/documents/:id
func (s *Server) getKnowledgeDoc(c *fiber.Ctx) error {
	d, err := s.deps.Knowledge.Get(c.UserContext(), s.currentUser(c), strings.Clone(c.Params("id")))
	if err != nil {
		return failKnowledge(c, err)
	}
	return c.JSON(knowledgeDocResponse{Document: *d})
}

// listKnowledgeChunks GET /api/knowledge/documents/:id/chunks?limit=&offset= — belge nasıl bölündü.
func (s *Server) listKnowledgeChunks(c *fiber.Ctx) error {
	limit, offset, err := s.parsePaging(c)
	if err != nil {
		return err
	}
	items, err := s.deps.Knowledge.Chunks(c.UserContext(), s.currentUser(c), strings.Clone(c.Params("id")), limit, offset)
	if err != nil {
		return failKnowledge(c, err)
	}
	return c.JSON(knowledgeChunksResponse{Items: items, Count: len(items), Limit: limit, Offset: offset})
}

// createKnowledgeDoc POST /api/knowledge/documents
//
// İki biçim kabul edilir:
//   - application/json: {"title","text","source","projectId","tags"}
//   - multipart/form-data: "file" alanı (+ isteğe bağlı title, source, projectId, tags=virgülle ayrık)
//
// Embedding arka planda üretilir: yanıttaki belge "indexing" olabilir ve anahtar kelimeyle
// hemen aranabilir. Aynı içerik ikinci kez yüklenirse 409.
func (s *Server) createKnowledgeDoc(c *fiber.Ctx) error {
	userID := s.currentUser(c)
	in, err := parseIngestBody(c)
	if err != nil {
		return err
	}
	in.UserID = userID
	doc, err := s.deps.Knowledge.Ingest(c.UserContext(), in)
	if err != nil {
		s.auditFailure(userID, "knowledge.create", err.Error())
		return failKnowledge(c, err)
	}
	s.auditSuccess(userID, "knowledge.create", "id="+doc.ID+" chunks="+strconv.Itoa(doc.ChunkCount))
	return c.Status(fiber.StatusCreated).JSON(knowledgeDocResponse{Document: *doc})
}

// deleteKnowledgeDoc DELETE /api/knowledge/documents/:id — belge ve parçaları silinir.
func (s *Server) deleteKnowledgeDoc(c *fiber.Ctx) error {
	userID, id := s.currentUser(c), strings.Clone(c.Params("id"))
	if err := s.deps.Knowledge.Delete(c.UserContext(), userID, id); err != nil {
		s.auditFailure(userID, "knowledge.delete", id+": "+err.Error())
		return failKnowledge(c, err)
	}
	s.auditSuccess(userID, "knowledge.delete", id)
	return c.SendStatus(fiber.StatusNoContent)
}

// searchKnowledge POST /api/knowledge/search — semantik/anahtar kelime/hibrit arama,
// ifade bonusu ve belge başına çeşitlilik sınırıyla yeniden sıralanmış parçalar.
// Okuma işlemidir; denetim kaydı alınmaz.
func (s *Server) searchKnowledge(c *fiber.Ctx) error {
	var in knowledgeSearchRequest
	if err := c.BodyParser(&in); err != nil {
		return fail(c, fiber.StatusBadRequest, protocol.ErrCodeBadRequest, "geçersiz istek gövdesi")
	}
	mode := in.Mode
	if mode == "" {
		mode = knowledge.ModeHybrid
	}
	hits, err := s.deps.Knowledge.Search(c.UserContext(), knowledge.SearchOptions{
		UserID: s.currentUser(c), Query: in.Query, Mode: in.Mode, Limit: in.Limit, MinScore: in.MinScore,
		ProjectID: in.ProjectID, DocumentIDs: in.DocumentIDs, Tags: in.Tags, MaxPerDocument: in.MaxPerDocument,
	})
	if err != nil {
		return failKnowledge(c, err)
	}
	if hits == nil {
		hits = []knowledge.Hit{} // JSON'da null değil []
	}
	return c.JSON(knowledgeSearchResponse{Items: hits, Count: len(hits), Mode: mode})
}

// reindexKnowledge POST /api/knowledge/reindex[?force=true] — eksik/bayat vektörleri arka planda üretir.
// force=true tümünü yeniler (embedding modeli değiştiğinde). Hemen döner; ilerleme belgelerin
// embeddedCount alanından izlenir.
func (s *Server) reindexKnowledge(c *fiber.Ctx) error {
	force := false
	if raw := strings.TrimSpace(c.Query("force")); raw != "" {
		v, err := strconv.ParseBool(raw)
		if err != nil {
			return fail(c, fiber.StatusBadRequest, protocol.ErrCodeBadRequest, "force true veya false olmalı")
		}
		force = v
	}
	userID := s.currentUser(c)
	rep, err := s.deps.Knowledge.Reindex(c.UserContext(), userID, force)
	if err != nil {
		s.auditFailure(userID, "knowledge.reindex", err.Error())
		return failKnowledge(c, err)
	}
	s.auditSuccess(userID, "knowledge.reindex", "force="+strconv.FormatBool(force)+" documents="+strconv.Itoa(rep.Documents))
	return c.Status(fiber.StatusAccepted).JSON(rep)
}

// parseIngestBody JSON ya da multipart gövdeyi IngestInput'a çevirir. Hata *fiber.Error'dur.
// Fiber v2 tuzağı: FormValue/FormFile istek tamponunu gösterir; kayıtta kalacak her değer kopyalanır.
func parseIngestBody(c *fiber.Ctx) (knowledge.IngestInput, error) {
	if strings.HasPrefix(strings.ToLower(c.Get(fiber.HeaderContentType)), fiber.MIMEMultipartForm) {
		fh, err := c.FormFile("file")
		if err != nil {
			return knowledge.IngestInput{}, fiber.NewError(fiber.StatusBadRequest, "multipart istekte 'file' alanı gerekli")
		}
		f, err := fh.Open()
		if err != nil {
			return knowledge.IngestInput{}, fiber.NewError(fiber.StatusBadRequest, "dosya açılamadı")
		}
		defer f.Close()
		data, err := io.ReadAll(f)
		if err != nil {
			return knowledge.IngestInput{}, fiber.NewError(fiber.StatusBadRequest, "dosya okunamadı")
		}
		var tags []string
		for _, t := range strings.Split(c.FormValue("tags"), ",") {
			tags = append(tags, strings.Clone(t))
		}
		return knowledge.IngestInput{
			ProjectID:   strings.Clone(c.FormValue("projectId")),
			Title:       strings.Clone(c.FormValue("title")),
			Source:      strings.Clone(c.FormValue("source")),
			Tags:        tags,
			Filename:    strings.Clone(fh.Filename),
			ContentType: strings.Clone(fh.Header.Get(fiber.HeaderContentType)),
			Data:        data,
		}, nil
	}

	var body knowledgeIngestRequest
	if err := c.BodyParser(&body); err != nil {
		return knowledge.IngestInput{}, fiber.NewError(fiber.StatusBadRequest, "geçersiz istek gövdesi")
	}
	return knowledge.IngestInput{
		ProjectID: body.ProjectID, Title: body.Title, Source: body.Source, Tags: body.Tags, Text: body.Text,
	}, nil
}

// failKnowledge Bilgi Tabanı hatalarını HTTP durumuna çevirir; kalanı failDomain'e bırakır.
func failKnowledge(c *fiber.Ctx, err error) error {
	var ve *knowledge.ValidationError
	var de *knowledge.DuplicateError
	var pe *llm.ProviderError
	switch {
	case errors.As(err, &ve):
		fields := make([]fieldError, 0, len(ve.Errors))
		for _, e := range ve.Errors {
			fields = append(fields, fieldError{Field: e.Field, Message: e.Message})
		}
		return c.Status(fiber.StatusBadRequest).JSON(errorBody{Error: apiError{
			Code: protocol.ErrCodeValidation, Message: "belge geçersiz", Fields: fields,
		}})
	case errors.As(err, &de):
		return fail(c, fiber.StatusConflict, protocol.ErrCodeConflict,
			"aynı içerik zaten yüklü (belge "+de.DocumentID+")")
	case errors.Is(err, knowledge.ErrNotFound):
		return fail(c, fiber.StatusNotFound, protocol.ErrCodeNotFound, "belge bulunamadı")
	case errors.Is(err, knowledge.ErrTooLarge), errors.Is(err, knowledge.ErrTooManyChunks):
		return fail(c, fiber.StatusRequestEntityTooLarge, protocol.ErrCodeTooLarge, err.Error())
	case errors.Is(err, knowledge.ErrUnsupportedFormat):
		return fail(c, fiber.StatusUnsupportedMediaType, protocol.ErrCodeBadRequest, err.Error())
	case errors.Is(err, knowledge.ErrEmptyText):
		return fail(c, fiber.StatusBadRequest, protocol.ErrCodeBadRequest, err.Error())
	case errors.Is(err, knowledge.ErrNoEmbedder), errors.Is(err, llm.ErrNoEmbedModel):
		return fail(c, fiber.StatusServiceUnavailable, protocol.ErrCodeLLM,
			"embedding sağlayıcısı yapılandırılmamış (OLLAMA_EMBED_MODEL ayarla)")
	case errors.As(err, &pe):
		return failLLM(c, err)
	default:
		return failDomain(c, err)
	}
}
