//go:build windows

package main

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"syscall"
	"time"
	"unsafe"

	"github.com/jchv/go-webview2"
	"golang.org/x/sys/windows"

	"vpnclient/internal/appui"
	"vpnclient/internal/config"
	"vpnclient/internal/core"
	"vpnclient/internal/netcheck"
	"vpnclient/internal/protocols/hysteria2"
	"vpnclient/internal/protocols/naive"
	"vpnclient/internal/update"
)

type app struct {
	mu           sync.Mutex
	mgr          *core.Manager
	cfgPath      string
	logBuf       *bytes.Buffer
	updateStatus update.Status
	pings        []netcheck.Result
	webview      webview2.WebView
}

type uiState struct {
	Connected     bool                 `json:"connected"`
	Profile       string               `json:"profile,omitempty"`
	ActiveProfile string               `json:"active_profile,omitempty"`
	Protocol      string               `json:"protocol,omitempty"`
	HTTPProxy     string               `json:"http_proxy,omitempty"`
	SOCKSProxy    string               `json:"socks_proxy,omitempty"`
	SystemProxy   bool                 `json:"system_proxy"`
	Proxy         string               `json:"proxy"`
	CoreReady     bool                 `json:"core_ready"`
	CorePath      string               `json:"core_path,omitempty"`
	ConfigPath    string               `json:"config_path"`
	Profiles      []config.ProfileInfo `json:"profiles"`
	Version       string               `json:"version"`
	Update        update.Status        `json:"update"`
	Pings         []netcheck.Result    `json:"pings"`
	Subscription  string               `json:"subscription_url"`
	Hy2           core.Hy2Options      `json:"hy2"`
}

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)

	cfgPath, err := config.LocateConfig()
	if err != nil {
		fatalDialog("Не удалось найти путь конфига: %v", err)
	}
	if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
		if err := config.WriteExample(cfgPath); err != nil {
			fatalDialog("Не удалось создать конфиг: %v", err)
		}
	}

	cfg, err := config.Load(cfgPath)
	if err != nil {
		fatalDialog("Ошибка конфига %s: %v", cfgPath, err)
	}

	logBuf := &bytes.Buffer{}
	mgr, err := core.NewManager(cfgPath, cfg, logBuf)
	if err != nil {
		fatalDialog("%v", err)
	}

	a := &app{
		mgr:     mgr,
		cfgPath: cfgPath,
		logBuf:  logBuf,
		updateStatus: update.Status{
			Current:     update.CurrentVersion,
			ManifestURL: update.DefaultManifestURL,
		},
	}

	dataPath := filepath.Join(filepath.Dir(cfgPath), ".navis-webview")
	_ = os.MkdirAll(dataPath, 0o755)

	go func() {
		_, _ = mgr.EnsureAllCores()
	}()

	w := webview2.NewWithOptions(webview2.WebViewOptions{
		Debug:     false,
		AutoFocus: true,
		DataPath:  dataPath,
		WindowOptions: webview2.WindowOptions{
			Title:  "Navis",
			Width:  520,
			Height: 860,
			Center: true,
			IconId: 1,
		},
	})
	if w == nil {
		fatalDialog("Не удалось создать окно WebView2.\nУстановите Microsoft Edge WebView2 Runtime.")
	}
	a.webview = w
	defer func() {
		_ = a.mgr.Disconnect()
		w.Destroy()
	}()

	applyWindowIcon(w.Window())
	w.SetSize(520, 860, webview2.HintNone)

	mustBind(w, "getState", a.getState)
	mustBind(w, "connect", a.connect)
	mustBind(w, "disconnect", a.disconnect)
	mustBind(w, "saveProfile", a.saveProfile)
	mustBind(w, "createProfile", a.createProfile)
	mustBind(w, "selectProfile", a.selectProfile)
	mustBind(w, "deleteProfile", a.deleteProfile)
	mustBind(w, "installCore", a.installCore)
	mustBind(w, "openURL", openURL)
	mustBind(w, "pingServers", a.pingServers)
	mustBind(w, "checkUpdate", a.checkUpdate)
	mustBind(w, "applyUpdate", a.applyUpdate)
	mustBind(w, "saveHy2", a.saveHy2)
	mustBind(w, "importSubscription", a.importSubscription)

	go a.autoCheckUpdate()

	w.SetHtml(appui.IndexHTML)
	w.Run()
}

func mustBind(w webview2.WebView, name string, fn interface{}) {
	if err := w.Bind(name, fn); err != nil {
		fatalDialog("bind %s: %v", name, err)
	}
}

func (a *app) getState() (uiState, error) {
	a.mu.Lock()
	defer a.mu.Unlock()

	st := a.mgr.Status()
	cfg := a.mgr.Config()
	proxy := ""
	active := cfg.Active
	if p, err := cfg.ActiveProfile(); err == nil {
		proxy = p.Proxy
		active = p.Name
	}
	corePath := ""
	coreReady := false
	if p, err := cfg.ActiveProfile(); err == nil {
		switch p.Protocol {
		case config.ProtocolHysteria2:
			if path, err := hysteria2.ResolveBinary(a.mgr.BinDir()); err == nil {
				corePath, coreReady = path, true
			}
		default:
			if path, err := naive.ResolveBinary(a.mgr.BinDir()); err == nil {
				corePath, coreReady = path, true
			}
		}
	} else if path, err := naive.ResolveBinary(a.mgr.BinDir()); err == nil {
		corePath, coreReady = path, true
	}
	out := uiState{
		Connected:     st.Connected,
		Profile:       st.Profile,
		ActiveProfile: active,
		Protocol:      string(st.Protocol),
		HTTPProxy:     st.HTTPProxy,
		SOCKSProxy:    st.SOCKSProxy,
		SystemProxy:   cfg.SystemProxy,
		Proxy:         proxy,
		CoreReady:     coreReady,
		CorePath:      corePath,
		ConfigPath:    a.cfgPath,
		Profiles:      cfg.ListProfiles(),
		Version:       update.CurrentVersion,
		Update:        a.updateStatus,
		Pings:         append([]netcheck.Result(nil), a.pings...),
		Subscription:  cfg.SubscriptionURL,
		Hy2:           a.mgr.ActiveHy2Options(),
	}
	if out.Protocol == "" {
		if p, err := cfg.ActiveProfile(); err == nil {
			out.Protocol = string(p.Protocol)
		}
	}
	if st.Connected {
		out.SystemProxy = st.SystemProxy
	}
	return out, nil
}

func (a *app) saveProfile(name, proxy string, systemProxy bool) error {
	a.mu.Lock()
	defer a.mu.Unlock()
	name = strings.TrimSpace(name)
	if name == "" {
		return fmt.Errorf("укажите название профиля")
	}
	a.mgr.SetSystemProxy(systemProxy)
	return a.mgr.SaveActiveProfile(name, proxy)
}

func (a *app) createProfile(name, proxy string, systemProxy bool) error {
	a.mu.Lock()
	defer a.mu.Unlock()
	name = strings.TrimSpace(name)
	if name == "" {
		return fmt.Errorf("укажите название профиля")
	}
	a.mgr.SetSystemProxy(systemProxy)
	return a.mgr.SaveProfile(name, proxy)
}

func (a *app) selectProfile(name string) error {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.mgr.SetActiveProfile(name)
}

func (a *app) deleteProfile(name string) error {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.mgr.DeleteProfile(name)
}

func (a *app) connect() error {
	a.mu.Lock()
	defer a.mu.Unlock()

	if p, err := a.mgr.Config().ActiveProfile(); err != nil {
		return err
	} else if strings.TrimSpace(p.Proxy) == "" {
		return fmt.Errorf("сначала вставьте ссылку сервера")
	}

	if _, err := a.mgr.EnsureCore(""); err != nil {
		return err
	}
	ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
	defer cancel()
	return a.mgr.Connect(ctx, "")
}

func (a *app) disconnect() error {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.mgr.Disconnect()
}

func (a *app) installCore() (string, error) {
	a.mu.Lock()
	defer a.mu.Unlock()
	paths, err := a.mgr.EnsureAllCores()
	if err != nil {
		return "", err
	}
	parts := make([]string, 0, len(paths))
	for k, v := range paths {
		parts = append(parts, k+": "+v)
	}
	return strings.Join(parts, " | "), nil
}

func (a *app) saveHy2(opts core.Hy2Options) error {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.mgr.SaveHy2Options(opts)
}

func (a *app) importSubscription(rawURL string) (int, error) {
	return a.mgr.ImportSubscription(rawURL)
}

func (a *app) pingServers() ([]netcheck.Result, error) {
	a.mu.Lock()
	list := a.mgr.Profiles()
	a.mu.Unlock()

	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()

	out := make([]netcheck.Result, 0, len(list))
	for _, p := range list {
		proxy := p.Proxy
		if proxy == "" {
			out = append(out, netcheck.Result{Name: p.Name, Error: "нет ссылки сервера"})
			continue
		}
		out = append(out, netcheck.PingProfile(ctx, p.Name, config.Protocol(p.Protocol), proxy))
	}

	a.mu.Lock()
	a.pings = out
	a.mu.Unlock()
	return out, nil
}

func (a *app) checkUpdate() (update.Status, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
	defer cancel()
	st, err := update.Check(ctx, update.DefaultManifestURL)
	a.mu.Lock()
	if err != nil {
		st.Current = update.CurrentVersion
		st.ManifestURL = update.DefaultManifestURL
		st.Error = err.Error()
	}
	a.updateStatus = st
	a.mu.Unlock()
	return st, nil
}

func (a *app) applyUpdate() error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()
	latest, err := update.Apply(ctx, update.DefaultManifestURL)
	if err != nil {
		return err
	}
	_ = a.mgr.Disconnect()
	go func() {
		time.Sleep(400 * time.Millisecond)
		if a.webview != nil {
			a.webview.Terminate()
		}
		os.Exit(0)
	}()
	_ = latest
	return nil
}

func (a *app) autoCheckUpdate() {
	time.Sleep(2 * time.Second)
	_, _ = a.checkUpdate()
	ticker := time.NewTicker(6 * time.Hour)
	defer ticker.Stop()
	for range ticker.C {
		_, _ = a.checkUpdate()
	}
}

func openURL(raw string) error {
	raw = strings.TrimSpace(raw)
	switch raw {
	case "https://evilfox.win/", "https://evilfox.win", "http://evilfox.win/", "http://evilfox.win":
		raw = "https://evilfox.win/"
	default:
		return fmt.Errorf("разрешена только ссылка evilfox.win")
	}
	cmd := exec.Command("rundll32", "url.dll,FileProtocolHandler", raw)
	cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
	return cmd.Start()
}

func applyWindowIcon(hwnd unsafe.Pointer) {
	ico := findIconPath()
	if ico == "" || hwnd == nil {
		return
	}
	pathPtr, err := windows.UTF16PtrFromString(ico)
	if err != nil {
		return
	}
	user32 := windows.NewLazySystemDLL("user32.dll")
	loadImage := user32.NewProc("LoadImageW")
	sendMessage := user32.NewProc("SendMessageW")
	const (
		imageIcon      = 1
		lrLoadFromFile = 0x0010
		lrDefaultSize  = 0x0040
		wmSetIcon      = 0x0080
		iconSmall      = 0
		iconBig        = 1
	)
	h, _, _ := loadImage.Call(0, uintptr(unsafe.Pointer(pathPtr)), imageIcon, 0, 0, lrLoadFromFile|lrDefaultSize)
	if h == 0 {
		return
	}
	sendMessage.Call(uintptr(hwnd), wmSetIcon, iconSmall, h)
	sendMessage.Call(uintptr(hwnd), wmSetIcon, iconBig, h)
}

func findIconPath() string {
	candidates := []string{}
	if exe, err := os.Executable(); err == nil {
		dir := filepath.Dir(exe)
		candidates = append(candidates,
			filepath.Join(dir, "assets", "navis.ico"),
			filepath.Join(dir, "navis.ico"),
		)
	}
	if cwd, err := os.Getwd(); err == nil {
		candidates = append(candidates, filepath.Join(cwd, "assets", "navis.ico"))
	}
	for _, c := range candidates {
		if st, err := os.Stat(c); err == nil && !st.IsDir() {
			return c
		}
	}
	return ""
}

func fatalDialog(format string, args ...interface{}) {
	msg := fmt.Sprintf(format, args...)
	log.Println(msg)
	messageBox(msg)
	os.Exit(1)
}
