package handlers

import (
	"context"
	"net/http"
	"net/url"
	"strings"

	"github.com/google/uuid"

	"mgit.msbls.de/m/paliad/internal/auth"
	"mgit.msbls.de/m/paliad/internal/checklists"
	"mgit.msbls.de/m/paliad/internal/services"
)

// Global search across every surface of Paliad — projects, deadlines, appointments,
// glossary, courts, checklist templates, link hub, users. The handler mixes
// in-memory scans (static curated content) with visibility-gated SQL for the
// tenant data. Each category is capped at maxPerCategory; overall output at
// maxTotalResults. Ranking is intentionally shallow — the caller types further
// if the list is too long.
const (
	maxPerCategory  = 5
	maxTotalResults = 20
	maxQueryRunes   = 128
)

// SearchResult is the shape every category returns to the frontend. id is a
// stable identifier (UUID, slug, or term string); type names the category;
// subtitle is optional secondary text; url is where the result navigates.
type SearchResult struct {
	ID       string `json:"id"`
	Type     string `json:"type"`
	Title    string `json:"title"`
	Subtitle string `json:"subtitle,omitempty"`
	URL      string `json:"url"`
}

// searchResponse bundles the grouped results. Empty categories are emitted
// as empty arrays so the client never has to check for `undefined`.
type searchResponse struct {
	Query        string         `json:"query"`
	Projects     []SearchResult `json:"projects"`
	Deadlines    []SearchResult `json:"deadlines"`
	Appointments []SearchResult `json:"appointments"`
	Glossary     []SearchResult `json:"glossary"`
	Courts       []SearchResult `json:"courts"`
	Checklists   []SearchResult `json:"checklists"`
	Links        []SearchResult `json:"links"`
	Users        []SearchResult `json:"users"`
}

// GET /api/search?q=<query>
func handleSearch(w http.ResponseWriter, r *http.Request) {
	q := strings.TrimSpace(r.URL.Query().Get("q"))
	if q == "" {
		writeJSON(w, http.StatusOK, emptySearchResponse())
		return
	}
	if n := len([]rune(q)); n > maxQueryRunes {
		q = string([]rune(q)[:maxQueryRunes])
	}
	needle := strings.ToLower(q)

	resp := emptySearchResponse()
	resp.Query = q
	resp.Glossary = searchGlossary(needle)
	resp.Courts = searchCourts(needle)
	resp.Checklists = searchChecklists(needle)
	resp.Links = searchLinks(needle)

	// DB-backed categories only light up when the database is wired AND the
	// caller is authenticated. The route lives behind the auth middleware so
	// a missing user ID means the JWT never carried one — fall back to the
	// static results rather than 401ing (the static hits are still useful).
	if dbSvc != nil {
		if uid, ok := auth.UserIDFromContext(r.Context()); ok {
			ctx := r.Context()
			resp.Projects = searchProjects(ctx, uid, q)
			resp.Deadlines = searchDeadlines(ctx, uid, needle)
			resp.Appointments = searchAppointments(ctx, uid, needle)
			resp.Checklists = append(resp.Checklists, searchChecklistInstances(ctx, uid, needle)...)
			resp.Users = searchUsers(ctx, needle)
		}
	}

	capCategory(&resp.Projects)
	capCategory(&resp.Deadlines)
	capCategory(&resp.Appointments)
	capCategory(&resp.Glossary)
	capCategory(&resp.Courts)
	capCategory(&resp.Checklists)
	capCategory(&resp.Links)
	capCategory(&resp.Users)
	capTotalBudget(&resp)

	writeJSON(w, http.StatusOK, resp)
}

func emptySearchResponse() searchResponse {
	return searchResponse{
		Projects:     []SearchResult{},
		Deadlines:    []SearchResult{},
		Appointments: []SearchResult{},
		Glossary:     []SearchResult{},
		Courts:       []SearchResult{},
		Checklists:   []SearchResult{},
		Links:        []SearchResult{},
		Users:        []SearchResult{},
	}
}

// ----------------------------------------------------------------------------
// Static-content searchers — scan curated Go slices, no DB.
// ----------------------------------------------------------------------------

func searchGlossary(needle string) []SearchResult {
	if needle == "" {
		return []SearchResult{}
	}
	out := make([]SearchResult, 0, maxPerCategory)
	for _, term := range glossaryTerms {
		if len(out) >= maxPerCategory {
			break
		}
		if matchesAny(needle, term.DE, term.EN, term.Definition) {
			out = append(out, SearchResult{
				ID:       term.DE,
				Type:     "glossary",
				Title:    term.DE,
				Subtitle: term.EN,
				URL:      "/glossary?q=" + url.QueryEscape(term.DE),
			})
		}
	}
	return out
}

func searchCourts(needle string) []SearchResult {
	if needle == "" {
		return []SearchResult{}
	}
	out := make([]SearchResult, 0, maxPerCategory)
	for _, c := range courts {
		if len(out) >= maxPerCategory {
			break
		}
		if matchesAny(needle, c.NameDE, c.NameEN, c.City, c.Type, c.Group, c.Country) {
			subtitle := c.City
			if c.Group != "" {
				if subtitle != "" {
					subtitle += " · " + c.Group
				} else {
					subtitle = c.Group
				}
			}
			out = append(out, SearchResult{
				ID:       c.ID,
				Type:     "court",
				Title:    c.NameDE,
				Subtitle: subtitle,
				URL:      "/courts?q=" + url.QueryEscape(c.NameDE),
			})
		}
	}
	return out
}

func searchChecklists(needle string) []SearchResult {
	if needle == "" {
		return []SearchResult{}
	}
	out := make([]SearchResult, 0, maxPerCategory)
	for _, tmpl := range checklists.Templates {
		if len(out) >= maxPerCategory {
			break
		}
		if matchesAny(needle, tmpl.TitleDE, tmpl.TitleEN, tmpl.DescriptionDE, tmpl.DescriptionEN, tmpl.Regime) {
			subtitle := tmpl.Regime
			if tmpl.CourtDE != "" {
				if subtitle != "" {
					subtitle += " · " + tmpl.CourtDE
				} else {
					subtitle = tmpl.CourtDE
				}
			}
			out = append(out, SearchResult{
				ID:       tmpl.Slug,
				Type:     "checklist_template",
				Title:    tmpl.TitleDE,
				Subtitle: subtitle,
				URL:      "/checklists/" + tmpl.Slug,
			})
		}
	}
	return out
}

func searchLinks(needle string) []SearchResult {
	if needle == "" {
		return []SearchResult{}
	}
	out := make([]SearchResult, 0, maxPerCategory)
	for _, l := range curatedLinks {
		if len(out) >= maxPerCategory {
			break
		}
		if matchesAny(needle, l.Title, l.DescDE, l.DescEN, l.Category) {
			out = append(out, SearchResult{
				ID:       l.ID,
				Type:     "link",
				Title:    l.Title,
				Subtitle: l.DescDE,
				URL:      "/links?q=" + url.QueryEscape(l.Title),
			})
		}
	}
	return out
}

// ----------------------------------------------------------------------------
// DB-backed searchers. All gate through the same visibility predicates the
// category's normal list endpoint already uses.
// ----------------------------------------------------------------------------

func searchProjects(ctx context.Context, uid uuid.UUID, q string) []SearchResult {
	rows, err := dbSvc.projects.List(ctx, uid, services.ProjectFilter{Search: q})
	if err != nil {
		return []SearchResult{}
	}
	out := make([]SearchResult, 0, maxPerCategory)
	for _, p := range rows {
		if len(out) >= maxPerCategory {
			break
		}
		subtitle := ""
		if p.Reference != nil && *p.Reference != "" {
			subtitle = *p.Reference
		}
		if p.ClientNumber != nil && *p.ClientNumber != "" {
			if subtitle != "" {
				subtitle += " · "
			}
			subtitle += *p.ClientNumber
			if p.MatterNumber != nil && *p.MatterNumber != "" {
				subtitle += "." + *p.MatterNumber
			}
		}
		if subtitle == "" {
			subtitle = humanProjectType(p.Type)
		}
		out = append(out, SearchResult{
			ID:       p.ID.String(),
			Type:     "project",
			Title:    p.Title,
			Subtitle: subtitle,
			URL:      "/projects/" + p.ID.String(),
		})
	}
	return out
}

// searchDeadlines filters the user's visible deadline set in Go. DeadlineService
// has no dedicated search method, but ListVisibleForUser already gates by team
// membership so the working set is bounded to what the user can see.
func searchDeadlines(ctx context.Context, uid uuid.UUID, needle string) []SearchResult {
	rows, err := dbSvc.deadline.ListVisibleForUser(ctx, uid, services.ListFilter{})
	if err != nil {
		return []SearchResult{}
	}
	out := make([]SearchResult, 0, maxPerCategory)
	for _, d := range rows {
		if len(out) >= maxPerCategory {
			break
		}
		note := ""
		if d.Notes != nil {
			note = *d.Notes
		}
		rule := ""
		if d.RuleCode != nil {
			rule = *d.RuleCode
		}
		if !matchesAny(needle, d.Title, note, rule) {
			continue
		}
		subtitle := d.ProjectTitle
		if !d.DueDate.IsZero() {
			if subtitle != "" {
				subtitle += " · "
			}
			subtitle += d.DueDate.Format("02.01.2006")
		}
		out = append(out, SearchResult{
			ID:       d.ID.String(),
			Type:     "deadline",
			Title:    d.Title,
			Subtitle: subtitle,
			URL:      "/deadlines/" + d.ID.String(),
		})
	}
	return out
}

func searchAppointments(ctx context.Context, uid uuid.UUID, needle string) []SearchResult {
	rows, err := dbSvc.appointment.ListVisibleForUser(ctx, uid, services.AppointmentListFilter{})
	if err != nil {
		return []SearchResult{}
	}
	out := make([]SearchResult, 0, maxPerCategory)
	for _, a := range rows {
		if len(out) >= maxPerCategory {
			break
		}
		desc := ""
		if a.Description != nil {
			desc = *a.Description
		}
		loc := ""
		if a.Location != nil {
			loc = *a.Location
		}
		if !matchesAny(needle, a.Title, loc, desc) {
			continue
		}
		subtitle := a.StartAt.Local().Format("02.01.2006 15:04")
		if loc != "" {
			subtitle += " · " + loc
		}
		out = append(out, SearchResult{
			ID:       a.ID.String(),
			Type:     "appointment",
			Title:    a.Title,
			Subtitle: subtitle,
			URL:      "/appointments/" + a.ID.String(),
		})
	}
	return out
}

func searchChecklistInstances(ctx context.Context, uid uuid.UUID, needle string) []SearchResult {
	out := make([]SearchResult, 0, maxPerCategory)
	for _, tmpl := range checklists.Templates {
		if len(out) >= maxPerCategory {
			break
		}
		rows, err := dbSvc.checklistInst.ListForTemplate(ctx, uid, tmpl.Slug)
		if err != nil {
			continue
		}
		for _, inst := range rows {
			if len(out) >= maxPerCategory {
				break
			}
			if !matchesAny(needle, inst.Name) {
				continue
			}
			subtitle := tmpl.TitleDE
			if inst.ProjectTitle != nil && *inst.ProjectTitle != "" {
				subtitle += " · " + *inst.ProjectTitle
			}
			out = append(out, SearchResult{
				ID:       inst.ID.String(),
				Type:     "checklist_instance",
				Title:    inst.Name,
				Subtitle: subtitle,
				URL:      "/checklists/instances/" + inst.ID.String(),
			})
		}
	}
	return out
}

func searchUsers(ctx context.Context, needle string) []SearchResult {
	rows, err := dbSvc.users.List(ctx)
	if err != nil {
		return []SearchResult{}
	}
	out := make([]SearchResult, 0, maxPerCategory)
	for _, u := range rows {
		if len(out) >= maxPerCategory {
			break
		}
		if !matchesAny(needle, u.DisplayName, u.Email, u.Office) {
			continue
		}
		subtitle := u.Email
		if u.Office != "" {
			subtitle += " · " + u.Office
		}
		out = append(out, SearchResult{
			ID:       u.ID.String(),
			Type:     "user",
			Title:    u.DisplayName,
			Subtitle: subtitle,
			// No dedicated user profile page exists yet; link to mail
			// so the entry is clickable and actionable.
			URL: "mailto:" + u.Email,
		})
	}
	return out
}

// ----------------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------------

// matchesAny returns true if the (already-lowercased) needle appears in any of
// the haystacks after lowercasing. Empty needle matches nothing.
func matchesAny(needle string, haystacks ...string) bool {
	if needle == "" {
		return false
	}
	for _, h := range haystacks {
		if h == "" {
			continue
		}
		if strings.Contains(strings.ToLower(h), needle) {
			return true
		}
	}
	return false
}

// capCategory trims a result slice to maxPerCategory in place.
func capCategory(rs *[]SearchResult) {
	if len(*rs) > maxPerCategory {
		*rs = (*rs)[:maxPerCategory]
	}
}

// capTotalBudget enforces the overall maxTotalResults ceiling. Trims from the
// largest category repeatedly so the final mix stays balanced across surfaces.
func capTotalBudget(r *searchResponse) {
	groups := []*[]SearchResult{
		&r.Projects, &r.Deadlines, &r.Appointments,
		&r.Glossary, &r.Courts, &r.Checklists,
		&r.Links, &r.Users,
	}
	total := 0
	for _, g := range groups {
		total += len(*g)
	}
	for total > maxTotalResults {
		biggest := -1
		biggestLen := 1
		for i, g := range groups {
			if len(*g) > biggestLen {
				biggest = i
				biggestLen = len(*g)
			}
		}
		if biggest < 0 {
			return
		}
		g := groups[biggest]
		*g = (*g)[:len(*g)-1]
		total--
	}
}

// humanProjectType maps the DB enum to a display label. Used when a project
// has no reference / client_number to show in the subtitle.
func humanProjectType(t string) string {
	switch t {
	case services.ProjectTypeClient:
		return "Mandant"
	case services.ProjectTypeLitigation:
		return "Litigation"
	case services.ProjectTypePatent:
		return "Patent"
	case services.ProjectTypeCase:
		return "Verfahren"
	case services.ProjectTypeProject:
		return "Projekt"
	case services.ProjectTypeOther:
		return "Sonstiges"
	}
	return t
}
