package httpapi

import (
	"context"
	"encoding/json"
	"log/slog"
	"net/http"
	"net/url"
	"strconv"

	"github.com/dtoro/oikos/internal/httpapi/gen"
	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
)

// serveRecentKnowledge backs the Knowledge page's "what the system knows / has
// learned" view (a custom route, not part of the generated OpenAPI surface).
// It returns recency-ordered knowledge with a small stats header so the
// operator can literally watch the knowledge base grow — especially the notes
// Nomos writes itself via upsert_knowledge (source='nomos-agent'), which is
// the concrete evidence of "the system is getting better." Optional ?source=
// and ?limit= query params.
func (s *Server) serveRecentKnowledge(w http.ResponseWriter, req *http.Request) {
	ctx := req.Context()
	limit := 50
	if l := req.URL.Query().Get("limit"); l != "" {
		if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 200 {
			limit = n
		}
	}
	source := req.URL.Query().Get("source") // "" = all, "nomos-agent" = agent-authored only

	type item struct {
		Slug          string   `json:"slug"`
		Title         string   `json:"title"`
		Kind          string   `json:"kind"`
		Source        string   `json:"source"`
		Tags          []string `json:"tags"`
		UpdatedAt     string   `json:"updated_at"`
		AgentAuthored bool     `json:"agent_authored"`
	}

	// updated_at is cast to text in SQL — pgx v5 can't scan a timestamptz
	// directly into a Go string (needs time.Time or an explicit cast), and
	// that scan error was being silently swallowed below (every row skipped,
	// endpoint returned 200 with an empty list and correct-looking stats
	// since the stats query doesn't scan any timestamp column — found live).
	rows, err := s.pool.Query(ctx, `
		SELECT e.slug, ke.title, e.type, COALESCE(ke.source,''), ke.tags, ke.updated_at::text
		FROM knowledge_entities ke
		JOIN entities e ON e.id = ke.entity_id
		WHERE ($1 = '' OR ke.source = $1)
		  AND ke.deleted_at IS NULL
		ORDER BY ke.updated_at DESC
		LIMIT $2`, source, limit)
	if err != nil {
		writeProblem(w, req, http.StatusInternalServerError, "query failed", err.Error())
		return
	}
	defer rows.Close()
	items := []item{}
	for rows.Next() {
		var it item
		var src string
		if err := rows.Scan(&it.Slug, &it.Title, &it.Kind, &src, &it.Tags, &it.UpdatedAt); err != nil {
			slog.Error("httpapi: knowledge/recent row scan failed", "error", err)
			continue
		}
		it.Source = src
		it.AgentAuthored = src == "nomos-agent"
		if it.Tags == nil {
			it.Tags = []string{}
		}
		items = append(items, it)
	}

	// Stats header: total, by kind, agent-authored, and how many changed in the
	// last 7 days (the "still learning" signal).
	var total, agentAuthored, last7d int
	byKind := map[string]int{}
	srows, err := s.pool.Query(ctx, `
		SELECT e.type, COUNT(*),
		       COUNT(*) FILTER (WHERE ke.source = 'nomos-agent'),
		       COUNT(*) FILTER (WHERE ke.updated_at > now() - interval '7 days')
		FROM knowledge_entities ke JOIN entities e ON e.id = ke.entity_id
		WHERE ke.deleted_at IS NULL
		GROUP BY e.type`)
	if err == nil {
		defer srows.Close()
		for srows.Next() {
			var kind string
			var c, a, l int
			if srows.Scan(&kind, &c, &a, &l) == nil {
				byKind[kind] = c
				total += c
				agentAuthored += a
				last7d += l
			}
		}
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]any{
		"stats": map[string]any{
			"total":          total,
			"by_kind":        byKind,
			"agent_authored": agentAuthored,
			"last_7d":        last7d,
		},
		"items": items,
	})
}

// serveKnowledgeContent returns the full markdown body for a document/
// investigation/runbook entity, by its own entity id or slug. Nothing else
// exposes knowledge_entities.content — GetEntityKnowledge (below) answers a
// different question ("what knowledge references THIS entity"), and
// SearchKnowledge only returns a short ts_headline snippet. The KB detail
// panel needs the entity's own full content when it IS a knowledge entity.
func (s *Server) serveKnowledgeContent(w http.ResponseWriter, req *http.Request) {
	ctx := req.Context()
	// chi.URLParam returns the raw, still-percent-encoded segment (unlike
	// the OpenAPI-generated routes, which decode via
	// runtime.BindStyledParameterWithOptions before reaching the handler) —
	// slugs like "document:containers/101-jellyfin" arrive as
	// "document%3Acontainers%2F101-jellyfin" and must be unescaped here.
	idOrSlug, err := url.PathUnescape(chi.URLParam(req, "id"))
	if err != nil {
		writeProblem(w, req, http.StatusBadRequest, "invalid id", err.Error())
		return
	}

	var title, content, source, editedBy string
	var tags []string
	var updatedAt string
	var revisions int
	err = s.pool.QueryRow(ctx, `
		SELECT ke.title, ke.content, COALESCE(ke.source,''), COALESCE(ke.edited_by,''),
		       ke.tags, ke.updated_at::text,
		       (SELECT count(*) FROM knowledge_revisions kr WHERE kr.entity_id = ke.entity_id)
		FROM knowledge_entities ke
		JOIN entities e ON e.id = ke.entity_id
		WHERE (e.slug = $1 OR e.id::text = $1)
		  AND ke.deleted_at IS NULL`, idOrSlug).
		Scan(&title, &content, &source, &editedBy, &tags, &updatedAt, &revisions)
	if err != nil {
		writeProblem(w, req, http.StatusNotFound, "no knowledge content for entity", "")
		return
	}
	if tags == nil {
		tags = []string{}
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]any{
		"title":      title,
		"content":    content,
		"source":     source,
		"edited_by":  editedBy,
		"tags":       tags,
		"updated_at": updatedAt,
		"revisions":  revisions,
	})
}

func (s *Server) SearchKnowledge(ctx context.Context, request gen.SearchKnowledgeRequestObject) (gen.SearchKnowledgeResponseObject, error) {
	q := request.Params.Q
	limit := clampLimit(request.Params.Limit)

	rows, err := s.pool.Query(ctx, `
		SELECT e.id, e.slug, COALESCE(et.name,''), ke.title, ke.source, ke.tags,
		       ts_rank(ke.search, plainto_tsquery('english', $1)) AS rank,
		       ts_headline('english', ke.content, plainto_tsquery('english', $1),
		         'MaxWords=40, MinWords=15, ShortWord=3, MaxFragments=3,
		          FragmentDelimiter=" ... "') AS snippet
		FROM knowledge_entities ke
		JOIN entities e ON e.id = ke.entity_id
		JOIN entity_types et ON et.name = e.type
		WHERE ke.search @@ plainto_tsquery('english', $1)
		  AND ke.deleted_at IS NULL
		ORDER BY rank DESC
		LIMIT $2`,
		q, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := []gen.KnowledgeHit{}

	for rows.Next() {
		var id uuid.UUID
		var slug, eType, title, source string
		var tags []string
		var rank float32
		var snippet *string

		if err := rows.Scan(&id, &slug, &eType, &title, &source, &tags, &rank, &snippet); err != nil {
			return nil, err
		}

		hitType := gen.Document
		switch eType {
		case "investigation":
			hitType = gen.Investigation
		case "runbook":
			hitType = gen.Runbook
		}

		items = append(items, gen.KnowledgeHit{
			Id:         id,
			Slug:       slug,
			Title:      title,
			Type:       hitType,
			Rank:       &rank,
			Snippet:    snippet,
			SourcePath: &source,
		})
	}
	if rows.Err() != nil {
		return nil, rows.Err()
	}

	if items == nil {
		items = []gen.KnowledgeHit{}
	}

	return gen.SearchKnowledge200JSONResponse{Items: items}, nil
}

func (s *Server) GetEntityKnowledge(ctx context.Context, request gen.GetEntityKnowledgeRequestObject) (gen.GetEntityKnowledgeResponseObject, error) {
	entitySlug := request.EntityId

	rows, err := s.pool.Query(ctx, `
		SELECT e.id, e.slug, COALESCE(et.name,''), ke.title, ke.source, ke.tags
		FROM knowledge_entities ke
		JOIN entities e ON e.id = ke.entity_id
		JOIN entity_types et ON et.name = e.type
		JOIN relationships r ON r.source_id = ke.entity_id
		JOIN entities target ON target.id = r.target_id
		WHERE target.slug = $1
		  AND r.valid_to IS NULL
		  AND r.type IN ('documents', 'about')
		  AND ke.deleted_at IS NULL
		UNION
		SELECT e.id, e.slug, COALESCE(et.name,''), ke.title, ke.source, ke.tags
		FROM knowledge_entities ke
		JOIN entities e ON e.id = ke.entity_id
		JOIN entity_types et ON et.name = e.type
		JOIN relationships r ON r.source_id = ke.entity_id
		JOIN entity_types target_type ON target_type.name = r.target_id::text
		JOIN entities ent ON ent.type = target_type.name AND ent.slug = $1
		WHERE r.valid_to IS NULL
		  AND r.type = 'procedure-for'
		  AND ke.deleted_at IS NULL
		ORDER BY 2`,
		entitySlug)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := []gen.KnowledgeHit{}
	for rows.Next() {
		var id uuid.UUID
		var slug, eType, title, source string
		var tags []string

		if err := rows.Scan(&id, &slug, &eType, &title, &source, &tags); err != nil {
			return nil, err
		}

		hitType := gen.Document
		switch eType {
		case "investigation":
			hitType = gen.Investigation
		case "runbook":
			hitType = gen.Runbook
		}

		items = append(items, gen.KnowledgeHit{
			Id:         id,
			Slug:       slug,
			Title:      title,
			Type:       hitType,
			SourcePath: &source,
		})
	}
	if rows.Err() != nil {
		return nil, rows.Err()
	}

	if items == nil {
		items = []gen.KnowledgeHit{}
	}

	return gen.GetEntityKnowledge200JSONResponse{Items: items}, nil
}