package main

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/gdamore/tcell/v3"
	"golang.org/x/term"
)

type State struct {
	mutex sync.Mutex
	data  map[string]string
}

var gState State

func init() {
	gState.data = make(map[string]string)
}

func run() {
	if gLogPath != "" {
		f, err := os.OpenFile(gLogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600)
		if err != nil {
			log.Fatalf("failed to open log file: %s", err)
		}
		defer f.Close()
		log.SetOutput(f)
	} else {
		log.SetOutput(io.Discard)
	}

	log.Printf("*************** starting client, PID: %d ***************", gClientID)

	var screen tcell.Screen
	var err error
	if screen, err = tcell.NewScreen(); err != nil {
		log.Fatalf("creating screen: %s", err)
	} else if err = screen.Init(); err != nil {
		log.Fatalf("initializing screen: %s", err)
	}
	if gOpts.mouse {
		screen.EnableMouse()
	}
	screen.EnablePaste()

	ui := newUI(screen)
	nav := newNav(ui)
	app := newApp(ui, nav)

	if err := nav.sync(); err != nil {
		app.ui.echoerrf("sync: %s", err)
	}

	if err := app.readHistory(); err != nil {
		app.ui.echoerrf("reading history file: %s", err)
	}

	app.loop()

	app.ui.screen.Fini()

	if gLastDirPath != "" {
		writeLastDir(gLastDirPath, app.nav.currDir().path)
	}

	if gSelectionPath != "" && len(app.selectionOut) > 0 {
		writeSelection(gSelectionPath, app.selectionOut)
	}

	if gPrintLastDir || gPrintSelection {
		stdoutIsTerminal := term.IsTerminal(int(os.Stdout.Fd()))

		if gPrintLastDir {
			printPath("last-dir", app.nav.currDir().path, stdoutIsTerminal)
		}

		if gPrintSelection {
			for _, file := range app.selectionOut {
				printPath("selection", file, stdoutIsTerminal)
			}
		}
	}
}

// printPath prints path for -print-last-dir / -print-selection. Newlines are
// rejected unconditionally (frame integrity for line-oriented consumers);
// control bytes are stripped only when stdout is a terminal.
func printPath(label, path string, stdoutIsTerminal bool) {
	if strings.ContainsAny(path, "\n\r") {
		log.Printf("%s: skipping path with newline: %q", label, path)
		return
	}
	if stdoutIsTerminal {
		path = sanitizeName(path)
	}
	fmt.Println(path)
}

func writeLastDir(filename, lastDir string) {
	if strings.ContainsAny(lastDir, "\n\r") {
		log.Printf("last-dir: path contains newline: %q", lastDir)
		return
	}
	f, err := os.Create(filename)
	if err != nil {
		log.Printf("opening last dir file: %s", err)
		return
	}
	defer f.Close()

	_, err = f.WriteString(lastDir)
	if err != nil {
		log.Printf("writing last dir file: %s", err)
	}
}

func writeSelection(filename string, selection []string) {
	f, err := os.Create(filename)
	if err != nil {
		log.Printf("opening selection file: %s", err)
		return
	}
	defer f.Close()

	filtered := slices.DeleteFunc(slices.Clone(selection), func(s string) bool {
		if strings.ContainsAny(s, "\n\r") {
			log.Printf("selection: skipping path with newline: %q", s)
			return true
		}
		return false
	})
	_, err = f.WriteString(strings.Join(filtered, "\n"))
	if err != nil {
		log.Printf("writing selection file: %s", err)
	}
}

func readExpr() <-chan expr {
	ch := make(chan expr)

	go func() {
		duration := 100 * time.Millisecond

		c, err := net.Dial("unix", gSocketPath)
		for err != nil {
			log.Printf("connecting server: %s", err)
			time.Sleep(duration)
			duration *= 2
			c, err = net.Dial("unix", gSocketPath)
		}

		if _, err := fmt.Fprintf(c, "conn %d\n", gClientID); err != nil {
			log.Printf("registering with server: %s", err)
			return
		}

		ch <- &callExpr{"sync", nil, 1}
		ch <- &callExpr{"on-init", nil, 1}

		s := bufio.NewScanner(c)
		for s.Scan() {
			log.Printf("recv: %s", s.Text())

			// `query` has to be handled outside of the main thread, which is
			// blocked when running a synchronous shell command ("$" or "!").
			// This is important since `query` is often the result of the user
			// running `$lf -remote "query $id <something>"`.
			if word, rest := splitWord(s.Text()); word == "query" {
				gState.mutex.Lock()
				state := gState.data[rest]
				gState.mutex.Unlock()
				if _, err := fmt.Fprintln(c, state); err != nil {
					log.Printf("sending response to server: %s", err)
					return
				}
			} else {
				p := newParser(strings.NewReader(s.Text()))
				if p.parse() {
					ch <- p.expr
				}
			}
		}

		if err := s.Err(); err != nil {
			log.Printf("reading from server: %s", err)
		}

		c.Close()
	}()

	return ch
}

func remote(req string) (string, error) {
	c, err := net.Dial("unix", gSocketPath)
	if err != nil {
		return "", fmt.Errorf("connecting to server: %w", err)
	}
	defer c.Close()

	if _, err := fmt.Fprintln(c, req); err != nil {
		return "", fmt.Errorf("sending command to server: %w", err)
	}

	// XXX: Standard net.Conn interface does not include a CloseWrite method
	// but net.UnixConn and net.TCPConn implement it so the following should be
	// safe as long as we do not use other types of connections. We need
	// CloseWrite to notify the server that this is not a persistent connection
	// and it should be closed after the response.
	switch c := c.(type) {
	case *net.TCPConn:
		c.CloseWrite()
	case *net.UnixConn:
		c.CloseWrite()
	}

	resp, err := io.ReadAll(c)
	if err != nil {
		return "", fmt.Errorf("reading response from server: %w", err)
	}

	return string(resp), nil
}
