package handlers

import (
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"

	"chat-switchboard/models"
	"chat-switchboard/notelinks"
	"chat-switchboard/store"
)

// ── Request / Response Types ────────────────

type createNoteRequest struct {
	Title           string   `json:"title" binding:"required"`
	Content         string   `json:"content" binding:"required"`
	FolderPath      string   `json:"folder_path"`
	Tags            []string `json:"tags"`
	SourceChannelID string   `json:"source_channel_id"`
	SourceMessageID string   `json:"source_message_id"`
}

type updateNoteRequest struct {
	Title      *string  `json:"title"`
	Content    *string  `json:"content"`
	FolderPath *string  `json:"folder_path"`
	Tags       []string `json:"tags"`
	// Mode controls how content is applied: "replace" (default), "append", "prepend"
	Mode string `json:"mode"`
}

type noteResponse struct {
	ID              string   `json:"id"`
	Title           string   `json:"title"`
	Content         string   `json:"content"`
	FolderPath      string   `json:"folder_path"`
	Tags            []string `json:"tags"`
	SourceChannelID *string  `json:"source_channel_id,omitempty"`
	SourceMessageID *string  `json:"source_message_id,omitempty"`
	CreatedAt       string   `json:"created_at"`
	UpdatedAt       string   `json:"updated_at"`
}

type noteListItem struct {
	ID         string   `json:"id"`
	Title      string   `json:"title"`
	FolderPath string   `json:"folder_path"`
	Tags       []string `json:"tags"`
	Preview    string   `json:"preview"`
	CreatedAt  string   `json:"created_at"`
	UpdatedAt  string   `json:"updated_at"`
}

type searchResult struct {
	noteListItem
	Rank     float64 `json:"rank"`
	Headline string  `json:"headline"`
}

// NoteHandler handles notes CRUD.
type NoteHandler struct {
	stores store.Stores
}

// NewNoteHandler creates a new handler.
func NewNoteHandler(s ...store.Stores) *NoteHandler {
	h := &NoteHandler{}
	if len(s) > 0 {
		h.stores = s[0]
	}
	return h
}

// toNoteResponse converts a models.Note to a noteResponse.
func toNoteResponse(n *models.Note) noteResponse {
	tags := n.Tags
	if tags == nil {
		tags = []string{}
	}
	return noteResponse{
		ID:              n.ID,
		Title:           n.Title,
		Content:         n.Content,
		FolderPath:      n.FolderPath,
		Tags:            tags,
		SourceChannelID: n.SourceChannelID,
		SourceMessageID: n.SourceMessageID,
		CreatedAt:       n.CreatedAt.Format("2006-01-02T15:04:05Z"),
		UpdatedAt:       n.UpdatedAt.Format("2006-01-02T15:04:05Z"),
	}
}

func toNoteListItem(n models.Note) noteListItem {
	tags := n.Tags
	if tags == nil {
		tags = []string{}
	}
	preview := n.Content
	if len(preview) > 200 {
		preview = preview[:200]
	}
	return noteListItem{
		ID:         n.ID,
		Title:      n.Title,
		FolderPath: n.FolderPath,
		Tags:       tags,
		Preview:    preview,
		CreatedAt:  n.CreatedAt.Format("2006-01-02T15:04:05Z"),
		UpdatedAt:  n.UpdatedAt.Format("2006-01-02T15:04:05Z"),
	}
}

// ── Create ──────────────────────────────────
// POST /api/v1/notes

func (h *NoteHandler) Create(c *gin.Context) {
	var req createNoteRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	userID := getUserID(c)

	folder := normalizeFolderPath(req.FolderPath)
	tags := req.Tags
	if tags == nil {
		tags = []string{}
	}

	var sourceChannelID *string
	if req.SourceChannelID != "" {
		sourceChannelID = &req.SourceChannelID
	}
	var sourceMessageID *string
	if req.SourceMessageID != "" {
		sourceMessageID = &req.SourceMessageID
	}

	note := &models.Note{
		UserID:          userID,
		Title:           req.Title,
		Content:         req.Content,
		FolderPath:      folder,
		Tags:            tags,
		SourceChannelID: sourceChannelID,
		SourceMessageID: sourceMessageID,
	}

	if err := h.stores.Notes.Create(c.Request.Context(), note); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create note"})
		return
	}

	// Extract and store wikilinks
	h.extractAndStoreLinks(c, note.ID, note.Content, userID)

	// Resolve dangling links from other notes that reference this title
	if h.stores.NoteLinks != nil {
		h.stores.NoteLinks.ResolveByTitle(c.Request.Context(), userID, note.ID, note.Title)
	}

	// Auto-associate note with source channel's project (v0.19.0)
	if req.SourceChannelID != "" && h.stores.Projects != nil {
		projID, _ := h.stores.Projects.GetProjectIDForChannel(
			c.Request.Context(), req.SourceChannelID)
		if projID != "" {
			_ = h.stores.Projects.AddNote(c.Request.Context(), projID, note.ID)
		}
	}

	c.JSON(http.StatusCreated, toNoteResponse(note))
}

// ── Get ─────────────────────────────────────
// GET /api/v1/notes/:id

func (h *NoteHandler) Get(c *gin.Context) {
	userID := getUserID(c)
	noteID := c.Param("id")

	note, err := h.stores.Notes.GetByID(c.Request.Context(), noteID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "note not found"})
		return
	}
	// Verify ownership
	if note.UserID != userID {
		c.JSON(http.StatusNotFound, gin.H{"error": "note not found"})
		return
	}

	c.JSON(http.StatusOK, toNoteResponse(note))
}

// ── Update ──────────────────────────────────
// PUT /api/v1/notes/:id

func (h *NoteHandler) Update(c *gin.Context) {
	var req updateNoteRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	userID := getUserID(c)
	noteID := c.Param("id")

	// Verify ownership
	existing, err := h.stores.Notes.GetByID(c.Request.Context(), noteID)
	if err != nil || existing.UserID != userID {
		c.JSON(http.StatusNotFound, gin.H{"error": "note not found"})
		return
	}

	// Build fields map
	fields := map[string]interface{}{}

	if req.Title != nil {
		fields["title"] = *req.Title
	}

	if req.Content != nil {
		mode := strings.ToLower(req.Mode)
		switch mode {
		case "append":
			fields["content"] = existing.Content + *req.Content
		case "prepend":
			fields["content"] = *req.Content + existing.Content
		default: // "replace" or empty
			fields["content"] = *req.Content
		}
	}

	if req.FolderPath != nil {
		fields["folder_path"] = normalizeFolderPath(*req.FolderPath)
	}

	if req.Tags != nil {
		fields["tags"] = req.Tags
	}

	if len(fields) == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
		return
	}

	if err := h.stores.Notes.Update(c.Request.Context(), noteID, fields); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update note"})
		return
	}

	// Re-fetch to get updated timestamps
	updated, err := h.stores.Notes.GetByID(c.Request.Context(), noteID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch updated note"})
		return
	}

	// Re-extract links if content changed
	if req.Content != nil {
		h.extractAndStoreLinks(c, noteID, updated.Content, userID)
	}

	c.JSON(http.StatusOK, toNoteResponse(updated))
}

// ── Delete ──────────────────────────────────
// DELETE /api/v1/notes/:id

func (h *NoteHandler) Delete(c *gin.Context) {
	userID := getUserID(c)
	noteID := c.Param("id")

	// Verify ownership
	existing, err := h.stores.Notes.GetByID(c.Request.Context(), noteID)
	if err != nil || existing.UserID != userID {
		c.JSON(http.StatusNotFound, gin.H{"error": "note not found"})
		return
	}

	if err := h.stores.Notes.Delete(c.Request.Context(), noteID); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete note"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"deleted": true})
}

// ── Bulk Delete ────────────────────────────
// POST /api/v1/notes/bulk-delete

func (h *NoteHandler) BulkDelete(c *gin.Context) {
	var req struct {
		IDs []string `json:"ids" binding:"required"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if len(req.IDs) == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "ids array is empty"})
		return
	}
	if len(req.IDs) > 100 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "max 100 notes per request"})
		return
	}

	userID := getUserID(c)

	count, err := h.stores.Notes.BulkDelete(c.Request.Context(), req.IDs, userID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete notes"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"deleted": count})
}

// GET /api/v1/notes?folder=/path&tag=sometag&limit=50&offset=0&sort=created_asc

func (h *NoteHandler) List(c *gin.Context) {
	userID := getUserID(c)
	folder := c.Query("folder")
	tag := c.Query("tag")
	sort := c.DefaultQuery("sort", "updated_desc")
	limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
	offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))

	if limit <= 0 || limit > 200 {
		limit = 50
	}

	opts := store.NoteListOptions{
		ListOptions: store.ListOptions{
			Limit:  limit,
			Offset: offset,
		},
		FolderPath: normalizeFolderPath(folder),
		Tag:        tag,
	}
	if folder == "" {
		opts.FolderPath = ""
	}

	// Map sort parameter
	switch sort {
	case "created_asc":
		opts.Sort = "created_at"
		opts.Order = "ASC"
	case "created_desc":
		opts.Sort = "created_at"
		opts.Order = "DESC"
	case "updated_asc":
		opts.Sort = "updated_at"
		opts.Order = "ASC"
	case "title_asc":
		opts.Sort = "title"
		opts.Order = "ASC"
	case "title_desc":
		opts.Sort = "title"
		opts.Order = "DESC"
	default: // "updated_desc"
		opts.Sort = ""
		opts.Order = ""
	}

	notes, total, err := h.stores.Notes.ListForUser(c.Request.Context(), userID, opts)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list notes"})
		return
	}

	items := make([]noteListItem, 0, len(notes))
	for _, n := range notes {
		items = append(items, toNoteListItem(n))
	}

	c.JSON(http.StatusOK, gin.H{
		"data":   items,
		"total":  total,
		"limit":  limit,
		"offset": offset,
	})
}

// ── Search ──────────────────────────────────
// GET /api/v1/notes/search?q=query&limit=20

func (h *NoteHandler) Search(c *gin.Context) {
	userID := getUserID(c)
	q := strings.TrimSpace(c.Query("q"))
	if q == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"})
		return
	}

	limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
	if limit <= 0 || limit > 100 {
		limit = 20
	}

	// SearchKeyword handles dialect internally: PG uses ts_rank/ts_headline, SQLite uses LIKE.
	storeResults, err := h.stores.Notes.SearchKeyword(c.Request.Context(), userID, q, limit)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "search failed"})
		return
	}

	results := make([]searchResult, 0, len(storeResults))
	for _, sr := range storeResults {
		tags := sr.Tags
		if tags == nil {
			tags = []string{}
		}
		results = append(results, searchResult{
			noteListItem: noteListItem{
				ID:         sr.ID,
				Title:      sr.Title,
				FolderPath: sr.FolderPath,
				Tags:       tags,
				Preview:    sr.Excerpt,
			},
			Rank:     sr.Rank,
			Headline: sr.Headline,
		})
	}

	c.JSON(http.StatusOK, gin.H{
		"data":  results,
		"query": q,
		"total": len(results),
	})
}

// ── List Folders ────────────────────────────
// GET /api/v1/notes/folders

func (h *NoteHandler) ListFolders(c *gin.Context) {
	userID := getUserID(c)

	storeFolders, err := h.stores.Notes.ListFolders(c.Request.Context(), userID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list folders"})
		return
	}

	type folderInfo struct {
		Path  string `json:"path"`
		Count int    `json:"count"`
	}
	folders := make([]folderInfo, 0, len(storeFolders))
	for _, f := range storeFolders {
		folders = append(folders, folderInfo{Path: f.Path, Count: f.Count})
	}

	c.JSON(http.StatusOK, gin.H{"data": folders})
}

// ── Helpers ─────────────────────────────────

// normalizeFolderPath ensures consistent folder path format.
func normalizeFolderPath(p string) string {
	p = strings.TrimSpace(p)
	if p == "" {
		return "/"
	}
	if !strings.HasPrefix(p, "/") {
		p = "/" + p
	}
	if !strings.HasSuffix(p, "/") {
		p = p + "/"
	}
	// Collapse double slashes
	for strings.Contains(p, "//") {
		p = strings.ReplaceAll(p, "//", "/")
	}
	return p
}

// ── Wikilink Endpoints ──────────────────────

// GET /api/v1/notes/search-titles?q=query&limit=10
func (h *NoteHandler) SearchTitles(c *gin.Context) {
	userID := getUserID(c)
	q := strings.TrimSpace(c.Query("q"))
	if q == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"})
		return
	}

	limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
	if limit <= 0 || limit > 50 {
		limit = 10
	}

	notes, err := h.stores.Notes.SearchTitles(c.Request.Context(), userID, q, limit)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "search failed"})
		return
	}

	type titleResult struct {
		ID    string `json:"id"`
		Title string `json:"title"`
	}

	results := make([]titleResult, 0, len(notes))
	for _, n := range notes {
		results = append(results, titleResult{ID: n.ID, Title: n.Title})
	}

	c.JSON(http.StatusOK, gin.H{"data": results})
}

// GET /api/v1/notes/:id/backlinks
func (h *NoteHandler) Backlinks(c *gin.Context) {
	userID := getUserID(c)
	noteID := c.Param("id")

	// Verify ownership
	note, err := h.stores.Notes.GetByID(c.Request.Context(), noteID)
	if err != nil || note.UserID != userID {
		c.JSON(http.StatusNotFound, gin.H{"error": "note not found"})
		return
	}

	if h.stores.NoteLinks == nil {
		c.JSON(http.StatusOK, gin.H{"data": []interface{}{}})
		return
	}

	results, err := h.stores.NoteLinks.Backlinks(c.Request.Context(), noteID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load backlinks"})
		return
	}
	if results == nil {
		results = []models.NoteLinkResult{}
	}

	c.JSON(http.StatusOK, gin.H{"data": results})
}

// GET /api/v1/notes/graph
func (h *NoteHandler) Graph(c *gin.Context) {
	userID := getUserID(c)

	if h.stores.NoteLinks == nil {
		c.JSON(http.StatusOK, models.NoteGraph{
			Nodes:      []models.NoteGraphNode{},
			Edges:      []models.NoteGraphEdge{},
			Unresolved: []models.NoteGraphDangling{},
		})
		return
	}

	graph, err := h.stores.NoteLinks.Graph(c.Request.Context(), userID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load graph"})
		return
	}

	c.JSON(http.StatusOK, graph)
}

// ── Link Extraction Helper ──────────────────

// extractAndStoreLinks parses [[wikilinks]] from content and stores them.
func (h *NoteHandler) extractAndStoreLinks(c *gin.Context, noteID, content, userID string) {
	if h.stores.NoteLinks == nil {
		return
	}

	links := notelinks.ExtractWikilinks(content)
	if len(links) == 0 {
		// Clear any existing links
		h.stores.NoteLinks.ReplaceLinks(c.Request.Context(), noteID, nil)
		return
	}

	// Resolve titles to note IDs
	ctx := c.Request.Context()
	for i, link := range links {
		notes, err := h.stores.Notes.SearchTitles(ctx, userID, link.TargetTitle, 1)
		if err == nil {
			for _, n := range notes {
				if strings.EqualFold(n.Title, link.TargetTitle) {
					id := n.ID
					links[i].TargetNoteID = &id
					break
				}
			}
		}
	}

	h.stores.NoteLinks.ReplaceLinks(ctx, noteID, links)
}
