package ui

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/textinput"

	"github.com/unkn0wn-root/resterm/internal/filesvc"
	"github.com/unkn0wn-root/resterm/internal/parser"
	"github.com/unkn0wn-root/resterm/internal/restfile"
	"github.com/unkn0wn-root/resterm/internal/ui/navigator"
	"github.com/unkn0wn-root/resterm/internal/util"
)

func (m *Model) rebuildNavigator(entries []filesvc.FileEntry) {
	m.cacheDoc(m.currentFile, m.doc)
	if len(entries) == 0 {
		entries = m.entriesFromList()
	}

	var prevNav *navigator.Model[any]
	if m.navigator != nil {
		prevNav = m.navigator
	}

	nodes := m.buildNavTree(entries)
	if prevNav != nil {
		applyNavigatorExpansion(nodes, prevNav)
	}
	if m.navigator == nil {
		m.navigator = navigator.New[any](nodes)
	} else {
		m.navigator.SetNodes(nodes)
	}
	m.navigator.SetCompact(m.navigatorCompact)
}

func navigatorRequestID(path string, idx int) string {
	return fmt.Sprintf("req:%s:%d", path, idx)
}

func navigatorWorkflowID(path string, idx int) string {
	return fmt.Sprintf("wf:%s:%d", path, idx)
}

func (m *Model) buildNavTree(entries []filesvc.FileEntry) []*navigator.Node[any] {
	if len(entries) == 0 {
		return nil
	}

	root := make([]*navigator.Node[any], 0, len(entries))
	dirs := make(map[string]*navigator.Node[any])
	add := func(p, c *navigator.Node[any]) {
		if p == nil {
			root = append(root, c)
			return
		}
		p.Children = append(p.Children, c)
	}
	for _, e := range entries {
		parts := relParts(e.Name)
		if len(parts) == 0 {
			continue
		}

		var p *navigator.Node[any]
		rel := ""
		for i := 0; i < len(parts)-1; i++ {
			rel = filepath.Join(rel, parts[i])
			d, ok := dirs[rel]
			if !ok {
				d = m.buildDirNode(parts[i], filepath.Join(m.workspaceRoot, rel))
				dirs[rel] = d
				add(p, d)
			}
			p = d
		}
		add(p, m.buildFileNode(e))
	}
	sortNavNodes(root)
	return root
}

func (m *Model) buildDirNode(name, path string) *navigator.Node[any] {
	return &navigator.Node[any]{
		ID:      "dir:" + path,
		Title:   name,
		Kind:    navigator.KindDir,
		Payload: navigator.Payload[any]{FilePath: path},
	}
}

func (m *Model) buildFileNode(entry filesvc.FileEntry) *navigator.Node[any] {
	id := "file:" + entry.Path
	node := &navigator.Node[any]{
		ID:      id,
		Title:   filepath.Base(entry.Name),
		Kind:    navigator.KindFile,
		Badges:  fileEntryBadges(entry, m.cfg.EnvironmentFile),
		Payload: navigator.Payload[any]{FilePath: entry.Path, Data: entry},
	}

	if !filesvc.IsRequestFile(entry.Path) {
		return node
	}

	if doc, ok := m.cachedDoc(entry.Path); ok && doc != nil {
		node.Count = len(doc.Requests)
		node.Children = m.buildRequestNodes(doc, entry.Path)
		if util.SamePath(entry.Path, m.currentFile) && navHasKids(node) {
			node.Expanded = true
		}
	}
	return node
}

func fileEntryBadges(entry filesvc.FileEntry, activeEnvFile string) []string {
	var badges []string
	if label := entry.Kind.BadgeLabel(); label != "" {
		badges = append(badges, label)
	}

	if entry.Kind == filesvc.FileKindEnv && util.SamePath(entry.Path, activeEnvFile) {
		badges = append(badges, "ACTIVE")
	}
	return badges
}

func (m *Model) buildRequestNodes(doc *restfile.Document, filePath string) []*navigator.Node[any] {
	if doc == nil || len(doc.Requests) == 0 {
		return nil
	}

	nodes := make([]*navigator.Node[any], 0, len(doc.Requests)+len(doc.Workflows))
	for idx, req := range doc.Requests {
		resolver := m.statusResolver(doc, req, m.cfg.EnvironmentName)
		title := requestNavLabel(req, resolver, fmt.Sprintf("Request %d", idx+1))
		method := strings.ToUpper(strings.TrimSpace(req.Method))
		desc := condense(expandStatusText(resolver, req.Metadata.Description), 80)
		target := expandStatusText(resolver, requestTarget(req))
		hasName := strings.TrimSpace(req.Metadata.Name) != ""
		if !hasName && strings.TrimSpace(title) == strings.TrimSpace(target) {
			target = ""
		}

		badges := requestBadges(req)
		nodes = append(nodes, &navigator.Node[any]{
			ID:      navigatorRequestID(filePath, idx),
			Title:   title,
			Desc:    desc,
			Kind:    navigator.KindRequest,
			Method:  method,
			Tags:    req.Metadata.Tags,
			Target:  target,
			Badges:  badges,
			HasName: hasName,
			Payload: navigator.Payload[any]{FilePath: filePath, Data: req},
		})
	}
	for idx := range doc.Workflows {
		wf := &doc.Workflows[idx]
		title := strings.TrimSpace(wf.Name)
		if title == "" {
			title = fmt.Sprintf("Workflow %d", idx+1)
		}

		desc := condense(strings.TrimSpace(wf.Description), 80)
		badges := []string{fmt.Sprintf("%d steps", len(wf.Steps))}
		nodes = append(nodes, &navigator.Node[any]{
			ID:      navigatorWorkflowID(filePath, idx),
			Title:   title,
			Desc:    desc,
			Kind:    navigator.KindWorkflow,
			Tags:    wf.Tags,
			Badges:  badges,
			Payload: navigator.Payload[any]{FilePath: filePath, Data: wf},
		})
	}
	return nodes
}

func (m *Model) cachedDoc(path string) (*restfile.Document, bool) {
	if path == "" {
		return nil, false
	}
	if path == m.currentFile && m.doc != nil {
		return m.doc, true
	}

	entry, ok := m.docCache[path]
	if !ok || entry.doc == nil {
		return nil, false
	}

	info, err := os.Stat(path)
	if err != nil || info.ModTime().After(entry.mod) {
		return nil, false
	}
	return entry.doc, true
}

func (m *Model) cacheDoc(path string, doc *restfile.Document) {
	if path == "" || doc == nil {
		return
	}

	info, err := os.Stat(path)
	mod := time.Time{}
	if err == nil {
		mod = info.ModTime()
	}
	if m.docCache == nil {
		m.docCache = make(map[string]navDocCache)
	}
	m.docCache[path] = navDocCache{doc: doc, mod: mod}
}

func (m *Model) ensureNavigatorFilter() {
	if m.navigatorFilter.Prompt == "" {
		m.navigatorFilter = newNavigatorFilterInput()
		m.themeRuntime.applyTextInput(&m.navigatorFilter, m.theme, textInputKindNavigator)
	}
}

func newNavigatorFilterInput() textinput.Model {
	ti := textinput.New()
	ti.Placeholder = "Requests, tags, files..."
	ti.Prompt = "Filter: "
	ti.CharLimit = 0
	ti.SetCursor(0)
	ti.Blur()
	return ti
}

func (m *Model) loadDocFor(path string) *restfile.Document {
	if path == "" {
		return nil
	}
	if path == m.currentFile && m.doc != nil {
		return m.doc
	}
	if doc, ok := m.cachedDoc(path); ok {
		return doc
	}

	data, err := os.ReadFile(path)
	if err != nil {
		m.setStatusMessage(statusMsg{text: fmt.Sprintf("open failed: %v", err), level: statusError})
		return nil
	}

	doc := parser.Parse(path, data)
	m.cacheDoc(path, doc)
	m.syncRegistry(doc)
	return doc
}

func (m *Model) expandNavigatorFile(path string) {
	if m.navigator == nil {
		return
	}
	if !filesvc.IsRequestFile(path) {
		return
	}

	doc := m.loadDocFor(path)
	if doc == nil {
		return
	}

	children := m.buildRequestNodes(doc, path)
	m.navigator.ReplaceChildrenAndExpand("file:"+path, children)
}

func (m *Model) ensureNavigatorRequestsForFile(path string) {
	if m.navigator == nil || path == "" {
		return
	}
	if !filesvc.IsRequestFile(path) {
		return
	}

	node := m.navigator.Find("file:" + path)
	if node == nil {
		return
	}

	if len(node.Children) == 0 {
		m.expandNavigatorFile(path)
		node = m.navigator.Find("file:" + path)
	}
	if node == nil {
		return
	}
	if navHasKids(node) {
		if !node.Expanded {
			node.Expanded = true
			m.navigator.Refresh()
		}
		return
	}
	if node.Expanded {
		node.Expanded = false
		m.navigator.Refresh()
	}
}

func (m *Model) ensureNavigatorDataForFilter() {
	if m.navigator == nil {
		return
	}

	filter := strings.TrimSpace(m.navigatorFilter.Value())
	need := filter != "" || len(m.navigator.MethodFilters()) > 0 ||
		len(m.navigator.TagFilters()) > 0
	if !need {
		return
	}
	for _, entry := range m.entriesFromList() {
		path := entry.Path
		if path == "" {
			continue
		}
		node := m.navigator.Find("file:" + path)
		if node == nil || len(node.Children) > 0 {
			continue
		}
		m.expandNavigatorFile(path)
	}
}

func (m *Model) entriesFromList() []filesvc.FileEntry {
	items := m.fileList.Items()
	out := make([]filesvc.FileEntry, 0, len(items))
	for _, it := range items {
		if fi, ok := it.(fileItem); ok {
			out = append(out, fi.entry)
		}
	}
	return out
}

func requestBadges(req *restfile.Request) []string {
	if req == nil {
		return nil
	}

	var b []string
	switch {
	case req.WebSocket != nil:
		b = append(b, "WS")
	case req.SSE != nil:
		b = append(b, "SSE")
	}

	if req.Metadata.Compare != nil {
		b = append(b, "CMP")
	}
	if req.Metadata.Auth != nil {
		b = append(b, "AUTH")
	}
	if len(req.Metadata.Scripts) > 0 {
		b = append(b, "SCRIPT")
	}
	return b
}

func (m *Model) syncNavigatorSelection() {
	if m.navigator == nil {
		return
	}

	n := m.navigator.Selected()
	m.syncNavigatorFocus(n)
	if n == nil {
		m.clearNavigatorRequestSelection()
		m.workflowList.Select(-1)
		return
	}

	path := n.Payload.FilePath
	switch n.Kind {
	case navigator.KindRequest:
		if req, ok := n.Payload.Data.(*restfile.Request); ok {
			if path != "" {
				_ = m.selectFileByPath(path)
			}
			if util.SamePath(path, m.currentFile) {
				m.setActiveRequest(req)
			} else {
				if m.pendingCrossFile.nodeID == n.ID {
					return
				}
				m.clearNavigatorRequestSelection()
				m.setStatusMessage(
					statusMsg{text: "Open file to edit this request", level: statusInfo},
				)
			}
		} else {
			m.clearNavigatorRequestSelection()
		}
	case navigator.KindWorkflow:
		if wf, ok := n.Payload.Data.(*restfile.Workflow); ok {
			if path != "" {
				_ = m.selectFileByPath(path)
			}
			if util.SamePath(path, m.currentFile) {
				if m.selectWorkflowForNode(wf, n.ID) {
					if item, ok := m.workflowList.SelectedItem().(workflowListItem); ok &&
						item.workflow != nil {
						wf = item.workflow
					}
				}
				m.workflowSelectionKey = workflowKey(wf)
			} else {
				if m.pendingCrossFile.nodeID == n.ID {
					return
				}
				m.clearNavigatorWorkflowSelection()
				m.setStatusMessage(
					statusMsg{text: "Open file to edit this workflow", level: statusInfo},
				)
			}
		} else {
			m.clearNavigatorWorkflowSelection()
		}
	case navigator.KindFile:
		if path != "" {
			_ = m.selectFileByPath(path)
		}
		m.clearNavigatorRequestSelection()
	default:
		m.clearNavigatorRequestSelection()
	}
}

func (m *Model) clearNavigatorRequestSelection() {
	m.setActiveRequest(nil)
	m.requestList.Select(-1)
}

func (m *Model) clearNavigatorWorkflowSelection() {
	m.workflowSelectionKey = ""
	m.workflowList.Select(-1)
}

func (m *Model) syncNavigatorFocus(n *navigator.Node[any]) {
	if n == nil {
		return
	}
	switch n.Kind {
	case navigator.KindRequest:
		_ = m.setFocus(focusRequests)
	case navigator.KindWorkflow:
		_ = m.setFocus(focusWorkflows)
	case navigator.KindFile, navigator.KindDir:
		_ = m.setFocus(focusFile)
	}
}

func (m *Model) applyCursorRequest(req *restfile.Request) {
	if req == nil {
		return
	}
	key := requestKey(req)
	if key != "" && key == m.activeRequestKey {
		m.currentRequest = req
		m.activeRequestTitle = requestDisplayName(req)
		_ = m.selectRequestItemByKey(key)
		return
	}
	m.setActiveRequest(req)
}

type cursorSyncState struct {
	line int
	file string
	doc  *restfile.Document
}

func (m *Model) resetCursorSync() {
	m.lastCursorSync = cursorSyncState{line: -1}
}

func (m *Model) markCursorSynced(line int) {
	m.lastCursorSync = m.currentCursorSyncState(line)
}

func (m *Model) currentCursorSyncState(line int) cursorSyncState {
	return cursorSyncState{
		line: line,
		file: m.currentFile,
		doc:  m.doc,
	}
}

func (m *Model) cursorSyncChanged(line int, targetID string) bool {
	// Doc pointer comparison intentionally detects reparses as a change.
	if m.lastCursorSync != m.currentCursorSyncState(line) {
		return true
	}

	currentID := ""
	if m.navigator != nil {
		if sel := m.navigator.Selected(); sel != nil {
			currentID = sel.ID
		}
	}
	return currentID != targetID
}

func (m *Model) syncNavigatorWithEditorCursor() {
	if m.navigator == nil || m.doc == nil || m.currentFile == "" {
		return
	}
	if m.focus != focusEditor {
		return
	}

	line := currentCursorLine(m.editor)

	if wf, wfIdx := workflowAtLine(m.doc, line); wf != nil {
		m.syncNavigatorWorkflowAtCursor(wf, wfIdx, line)
		return
	}

	req, reqIdx := requestAtLine(m.doc, line)
	if req == nil && len(m.doc.Requests) > 0 {
		lastIdx := len(m.doc.Requests) - 1
		last := m.doc.Requests[lastIdx]
		if last != nil && line > last.LineRange.End {
			req = last
			reqIdx = lastIdx
		}
	}

	if req == nil {
		m.markCursorSynced(line)
		return
	}

	targetID := navigatorRequestID(m.currentFile, reqIdx)
	if !m.cursorSyncChanged(line, targetID) {
		return
	}

	m.ensureNavigatorRequestsForFile(m.currentFile)
	if !m.navigator.SelectByID(targetID) {
		m.markCursorSynced(line)
		return
	}

	m.applyCursorRequest(req)
	m.markCursorSynced(line)
}

func (m *Model) syncNavigatorWorkflowAtCursor(wf *restfile.Workflow, wfIdx int, line int) {
	if wf == nil || wfIdx < 0 {
		return
	}

	targetID := navigatorWorkflowID(m.currentFile, wfIdx)
	if !m.cursorSyncChanged(line, targetID) {
		return
	}

	m.ensureNavigatorRequestsForFile(m.currentFile)
	if !m.navigator.SelectByID(targetID) {
		m.markCursorSynced(line)
		return
	}

	if m.selectWorkflowForNode(wf, targetID) {
		if item, ok := m.workflowList.SelectedItem().(workflowListItem); ok &&
			item.workflow != nil {
			wf = item.workflow
		}
	}
	m.workflowSelectionKey = workflowKey(wf)
	m.markCursorSynced(line)
}

func workflowAtLine(doc *restfile.Document, line int) (*restfile.Workflow, int) {
	if doc == nil || line < 1 {
		return nil, -1
	}
	for idx := range doc.Workflows {
		wf := &doc.Workflows[idx]
		start := wf.LineRange.Start
		end := wf.LineRange.End
		if start < 1 {
			continue
		}
		if end < start {
			end = start
		}
		if line >= start && line <= end {
			return wf, idx
		}
	}
	return nil, -1
}

// applyNavigatorExpansion copies expanded state from the previous navigator tree.
func applyNavigatorExpansion(nodes []*navigator.Node[any], prev *navigator.Model[any]) {
	if prev == nil || len(nodes) == 0 {
		return
	}

	var walk func(n *navigator.Node[any])
	walk = func(n *navigator.Node[any]) {
		if n == nil {
			return
		}
		if old := prev.Find(n.ID); old != nil && old.Expanded && navHasKids(n) {
			n.Expanded = true
		}
		for _, c := range n.Children {
			walk(c)
		}
	}
	for _, n := range nodes {
		walk(n)
	}
}

func navHasKids(n *navigator.Node[any]) bool {
	return n != nil && len(n.Children) > 0
}

func sortNavNodes(nodes []*navigator.Node[any]) {
	if len(nodes) == 0 {
		return
	}
	sort.Slice(nodes, func(i, j int) bool {
		a := nodes[i]
		b := nodes[j]
		wa := navKindRank(a)
		wb := navKindRank(b)
		if wa != wb {
			return wa < wb
		}
		at := ""
		bt := ""
		if a != nil {
			at = a.Title
		}
		if b != nil {
			bt = b.Title
		}
		if at == bt {
			if a == nil || b == nil {
				return a != nil
			}
			return a.ID < b.ID
		}
		return at < bt
	})
	for _, n := range nodes {
		if n != nil && n.Kind == navigator.KindDir {
			sortNavNodes(n.Children)
		}
	}
}

func navKindRank(n *navigator.Node[any]) int {
	if n == nil {
		return 2
	}
	switch n.Kind {
	case navigator.KindDir:
		return 0
	case navigator.KindFile:
		return 1
	default:
		return 2
	}
}

func relParts(name string) []string {
	clean := filepath.Clean(name)
	if clean == "" || clean == "." {
		return nil
	}

	clean = filepath.ToSlash(clean)
	parts := strings.Split(clean, "/")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		if p == "" || p == "." {
			continue
		}
		out = append(out, p)
	}
	return out
}
