package core

import (
	"context"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync"
	"time"

	"vpnclient/internal/config"
	"vpnclient/internal/linknorm"
	"vpnclient/internal/protocols/hysteria2"
	"vpnclient/internal/protocols/naive"
	"vpnclient/internal/sysproxy"
)

// Manager orchestrates protocol engines and Windows system proxy.
type Manager struct {
	mu       sync.Mutex
	cfgPath  string
	cfg      *config.Config
	engine   Engine
	sys      sysproxy.Controller
	profile  *config.Profile
	stderr   io.Writer
	binDir   string
}

func NewManager(cfgPath string, cfg *config.Config, stderr io.Writer) (*Manager, error) {
	if stderr == nil {
		stderr = os.Stderr
	}
	binDir, err := config.ResolveBinDir(cfgPath, cfg.BinDir)
	if err != nil {
		return nil, err
	}
	return &Manager{
		cfgPath: cfgPath,
		cfg:     cfg,
		sys:     sysproxy.New(),
		stderr:  stderr,
		binDir:  binDir,
	}, nil
}

func (m *Manager) Connect(ctx context.Context, profileName string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.engine != nil && m.engine.Running() {
		return fmt.Errorf("already connected; disconnect first")
	}

	if profileName != "" {
		m.cfg.Active = profileName
	}
	profile, err := m.cfg.ActiveProfile()
	if err != nil {
		return err
	}

	engine, err := m.newEngine(profile.Protocol)
	if err != nil {
		return err
	}

	if err := engine.Start(ctx, *profile, m.binDir); err != nil {
		return err
	}

	if m.cfg.SystemProxy {
		httpProxy, ok := engine.LocalHTTPProxy()
		if !ok {
			_ = engine.Stop()
			return fmt.Errorf("system_proxy requires an http:// listen address in the profile")
		}
		if err := m.sys.Enable(httpProxy); err != nil {
			_ = engine.Stop()
			return fmt.Errorf("enable system proxy: %w", err)
		}
	}

	m.engine = engine
	m.profile = profile
	return nil
}

func (m *Manager) Disconnect() error {
	m.mu.Lock()
	defer m.mu.Unlock()

	var first error
	if m.sys.Enabled() {
		if err := m.sys.Disable(); err != nil && first == nil {
			first = err
		}
	}
	if m.engine != nil {
		if err := m.engine.Stop(); err != nil && first == nil {
			first = err
		}
		m.engine = nil
	}
	m.profile = nil
	return first
}

func (m *Manager) Status() Status {
	m.mu.Lock()
	defer m.mu.Unlock()

	st := Status{SystemProxy: m.sys.Enabled()}
	if m.engine == nil || !m.engine.Running() {
		return st
	}
	st.Connected = true
	if m.profile != nil {
		st.Profile = m.profile.Name
		st.Protocol = m.profile.Protocol
	}
	if hp, ok := m.engine.LocalHTTPProxy(); ok {
		st.HTTPProxy = hp
	}
	if sp, ok := m.engine.LocalSOCKSProxy(); ok {
		st.SOCKSProxy = sp
	}
	return st
}

// Probe fetches url via the local HTTP or SOCKS proxy to verify the tunnel.
func (m *Manager) Probe(ctx context.Context, testURL string) error {
	m.mu.Lock()
	engine := m.engine
	m.mu.Unlock()
	if engine == nil || !engine.Running() {
		return fmt.Errorf("not connected")
	}
	if testURL == "" {
		testURL = "https://www.google.com/generate_204"
	}

	var proxyURL *url.URL
	if hp, ok := engine.LocalHTTPProxy(); ok {
		proxyURL, _ = url.Parse("http://" + hp)
	} else if sp, ok := engine.LocalSOCKSProxy(); ok {
		proxyURL, _ = url.Parse("socks5://" + sp)
	} else {
		return fmt.Errorf("no local proxy listen address")
	}

	transport := &http.Transport{
		Proxy: http.ProxyURL(proxyURL),
		DialContext: (&net.Dialer{
			Timeout: 15 * time.Second,
		}).DialContext,
		TLSHandshakeTimeout: 15 * time.Second,
		ForceAttemptHTTP2:   true,
	}
	client := &http.Client{
		Transport: transport,
		Timeout:   30 * time.Second,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, testURL, nil)
	if err != nil {
		return err
	}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("probe via %s: %w", proxyURL.String(), err)
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 400 {
		return fmt.Errorf("probe unexpected status %d", resp.StatusCode)
	}
	return nil
}

func (m *Manager) BinDir() string { return m.binDir }

func (m *Manager) ConfigPath() string { return m.cfgPath }

func (m *Manager) Config() *config.Config {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.cfg
}

func (m *Manager) SetSystemProxy(enabled bool) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.cfg.SystemProxy = enabled
}

func (m *Manager) UpdateProxyURI(proxy string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	active, err := m.cfg.ActiveProfile()
	if err != nil {
		return err
	}
	normalized, proto, _, err := linknorm.Normalize(active.Protocol, proxy)
	if err != nil {
		return err
	}
	if err := m.cfg.SetActiveProxy(normalized); err != nil {
		return err
	}
	if proto != "" {
		_ = m.cfg.UpsertProfileWithProtocol(m.cfg.Active, normalized, proto)
	}
	return config.Save(m.cfgPath, *m.cfg)
}

func (m *Manager) SetActiveProfile(name string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.engine != nil && m.engine.Running() {
		return fmt.Errorf("сначала отключитесь")
	}
	if err := m.cfg.SetActive(name); err != nil {
		return err
	}
	return config.Save(m.cfgPath, *m.cfg)
}

func (m *Manager) SaveProfile(name, proxy string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.engine != nil && m.engine.Running() {
		return fmt.Errorf("сначала отключитесь")
	}
	proxy = strings.TrimSpace(proxy)
	var proto config.Protocol
	if proxy != "" {
		normalized, detected, _, err := linknorm.Normalize("", proxy)
		if err != nil {
			return err
		}
		proxy = normalized
		proto = detected
	}
	if err := m.cfg.UpsertProfileWithProtocol(name, proxy, proto); err != nil {
		return err
	}
	return config.Save(m.cfgPath, *m.cfg)
}

// SaveActiveProfile updates the current profile (rename if needed) and proxy URI.
func (m *Manager) SaveActiveProfile(name, proxy string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.engine != nil && m.engine.Running() {
		return fmt.Errorf("сначала отключитесь")
	}
	name = strings.TrimSpace(name)
	if name == "" {
		return fmt.Errorf("имя профиля пустое")
	}
	proxy = strings.TrimSpace(proxy)
	var proto config.Protocol
	if proxy != "" {
		activeProto := config.Protocol("")
		if p, err := m.cfg.ActiveProfile(); err == nil {
			activeProto = p.Protocol
		}
		normalized, detected, _, err := linknorm.Normalize(activeProto, proxy)
		if err != nil {
			return err
		}
		proxy = normalized
		proto = detected
	}

	active := m.cfg.Active
	if name != active {
		exists := false
		for _, p := range m.cfg.Profiles {
			if p.Name == name {
				exists = true
				break
			}
		}
		if exists {
			if err := m.cfg.SetActive(name); err != nil {
				return err
			}
		} else if err := m.cfg.RenameProfile(active, name); err != nil {
			return err
		}
	}
	if err := m.cfg.UpsertProfileWithProtocol(name, proxy, proto); err != nil {
		return err
	}
	return config.Save(m.cfgPath, *m.cfg)
}

func (m *Manager) DeleteProfile(name string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.engine != nil && m.engine.Running() {
		return fmt.Errorf("сначала отключитесь")
	}
	if err := m.cfg.DeleteProfile(name); err != nil {
		return err
	}
	return config.Save(m.cfgPath, *m.cfg)
}

func (m *Manager) Profiles() []config.ProfileInfo {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.cfg.ListProfiles()
}

func (m *Manager) SaveConfig() error {
	m.mu.Lock()
	defer m.mu.Unlock()
	return config.Save(m.cfgPath, *m.cfg)
}

func (m *Manager) Reload() error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.engine != nil && m.engine.Running() {
		return fmt.Errorf("disconnect before reloading config")
	}
	cfg, err := config.Load(m.cfgPath)
	if err != nil {
		return err
	}
	binDir, err := config.ResolveBinDir(m.cfgPath, cfg.BinDir)
	if err != nil {
		return err
	}
	m.cfg = cfg
	m.binDir = binDir
	return nil
}

func (m *Manager) newEngine(p config.Protocol) (Engine, error) {
	switch p {
	case config.ProtocolNaive, "":
		return naive.New(m.stderr), nil
	case config.ProtocolHysteria2:
		return hysteria2.New(m.stderr), nil
	default:
		return nil, fmt.Errorf("unsupported protocol %q", p)
	}
}

// EnsureCore downloads the binary required by the active (or given) protocol.
func (m *Manager) EnsureCore(proto config.Protocol) (string, error) {
	if proto == "" {
		if p, err := m.cfg.ActiveProfile(); err == nil {
			proto = p.Protocol
		}
	}
	switch proto {
	case config.ProtocolHysteria2:
		return hysteria2.EnsureBinary(m.binDir)
	default:
		return naive.EnsureBinary(m.binDir)
	}
}

// EnsureAllCores installs naive + hysteria2 cores.
func (m *Manager) EnsureAllCores() (map[string]string, error) {
	out := map[string]string{}
	p1, err := naive.EnsureBinary(m.binDir)
	if err != nil {
		return out, fmt.Errorf("naive: %w", err)
	}
	out["naive"] = p1
	p2, err := hysteria2.EnsureBinary(m.binDir)
	if err != nil {
		return out, fmt.Errorf("hysteria2: %w", err)
	}
	out["hysteria2"] = p2
	return out, nil
}
