package exportproxy

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"net"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"vocat/internal/store"
)

const (
	SettingKey   = "developer.export_proxy.configs"
	PasswordMask = "••••••••"
	ReservedID   = "export-proxy"
)

var (
	ErrNotFound = errors.New("export proxy configuration not found")
	ErrDisabled = errors.New("export proxy is disabled")
)

type Config struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	DeviceID    string `json:"device_id"`
	Interface   string `json:"interface"`
	Mode        string `json:"mode"`
	ListenHost  string `json:"listen_host"`
	ListenPort  int    `json:"listen_port"`
	Enabled     bool   `json:"enabled"`
	AuthEnabled bool   `json:"auth_enabled"`
	Username    string `json:"username"`
	Password    string `json:"password"`
}

type Status struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	Mode      string    `json:"mode"`
	Enabled   bool      `json:"enabled"`
	Running   bool      `json:"running"`
	Listen    string    `json:"listen"`
	Error     string    `json:"error,omitempty"`
	StartedAt time.Time `json:"started_at,omitempty"`
}

type Manager struct {
	mu        sync.Mutex
	store     *store.Store
	logger    *slog.Logger
	configs   []Config
	listeners map[string]net.Listener
	started   map[string]time.Time
	lastError map[string]string
	disabled  bool
}

func New(ctx context.Context, database *store.Store, logger *slog.Logger, legacyConfigPath string) (*Manager, error) {
	if database == nil {
		return nil, errors.New("export proxy store is required")
	}
	if logger == nil {
		logger = slog.Default()
	}
	manager := &Manager{
		store: database, logger: logger,
		listeners: make(map[string]net.Listener),
		started:   make(map[string]time.Time),
		lastError: make(map[string]string),
	}
	migrated, err := manager.load(ctx, legacyConfigPath)
	if err != nil {
		return nil, err
	}
	if migrated {
		if err := manager.saveLocked(ctx); err != nil {
			return nil, fmt.Errorf("migrate legacy export proxy configurations: %w", err)
		}
		_ = RemoveLegacyConfig(legacyConfigPath)
	}

	for _, config := range manager.configs {
		if config.Enabled {
			if err := manager.start(ctx, config.ID); err != nil {
				manager.logger.Warn("start built-in export proxy", "id", config.ID, "error", err)
			}
		}
	}
	return manager, nil
}

func RemoveLegacyConfig(path string) error {
	path = strings.TrimSpace(path)
	if path == "" {
		return nil
	}
	err := os.Remove(path)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	return err
}

func (manager *Manager) load(ctx context.Context, legacyConfigPath string) (bool, error) {
	setting, err := manager.store.AppSetting(ctx, SettingKey)
	if err == nil {
		if err := json.Unmarshal(setting.Value, &manager.configs); err != nil {
			return false, fmt.Errorf("decode export proxy configurations: %w", err)
		}
		return false, nil
	}
	if !errors.Is(err, store.ErrNotFound) {
		return false, err
	}
	legacy, err := os.ReadFile(strings.TrimSpace(legacyConfigPath))
	if err != nil {
		if errors.Is(err, os.ErrNotExist) || strings.TrimSpace(legacyConfigPath) == "" {
			return false, nil
		}
		return false, err
	}
	if err := json.Unmarshal(legacy, &manager.configs); err != nil {
		return false, fmt.Errorf("decode legacy export proxy configurations: %w", err)
	}
	return true, nil
}

func (manager *Manager) saveLocked(ctx context.Context) error {
	raw, err := json.Marshal(manager.configs)
	if err != nil {
		return err
	}
	return manager.store.UpsertAppSetting(ctx, store.AppSetting{Key: SettingKey, Value: raw, Sensitive: true})
}

func (manager *Manager) Configs() ([]Config, error) {
	manager.mu.Lock()
	defer manager.mu.Unlock()
	if manager.disabled {
		return nil, ErrDisabled
	}
	result := make([]Config, len(manager.configs))
	for index, config := range manager.configs {
		result[index] = redact(config)
	}
	return result, nil
}

// EnabledConfigForDevice returns the first enabled configuration bound to the
// given device, reporting whether one exists. It is used to block turning off a
// device's roaming data while one of its export proxies is still running.
func (manager *Manager) EnabledConfigForDevice(deviceID string) (Config, bool) {
	manager.mu.Lock()
	defer manager.mu.Unlock()
	if manager.disabled {
		return Config{}, false
	}
	for _, config := range manager.configs {
		if config.DeviceID == deviceID && config.Enabled {
			return redact(config), true
		}
	}
	return Config{}, false
}

func (manager *Manager) Status() ([]Status, error) {
	manager.mu.Lock()
	defer manager.mu.Unlock()
	if manager.disabled {
		return nil, ErrDisabled
	}
	result := make([]Status, 0, len(manager.configs))
	for _, config := range manager.configs {
		status := Status{ID: config.ID, Name: config.Name, Mode: config.Mode, Enabled: config.Enabled, Error: manager.lastError[config.ID]}
		if listener := manager.listeners[config.ID]; listener != nil {
			status.Running = true
			status.Listen = listener.Addr().String()
			status.StartedAt = manager.started[config.ID]
		}
		result = append(result, status)
	}
	return result, nil
}

func (manager *Manager) Create(ctx context.Context, config Config) (Config, error) {
	config.ID = generateID()
	if err := manager.prepareConfig(ctx, &config); err != nil {
		return Config{}, err
	}
	manager.mu.Lock()
	if manager.disabled {
		manager.mu.Unlock()
		return Config{}, ErrDisabled
	}
	if err := manager.checkPortLocked(config, ""); err != nil {
		manager.mu.Unlock()
		return Config{}, err
	}
	manager.configs = append(manager.configs, config)
	if err := manager.saveLocked(ctx); err != nil {
		manager.configs = manager.configs[:len(manager.configs)-1]
		manager.mu.Unlock()
		return Config{}, err
	}
	manager.mu.Unlock()
	if config.Enabled {
		if err := manager.start(ctx, config.ID); err != nil {
			_ = manager.Delete(context.Background(), config.ID)
			return Config{}, err
		}
	}
	return redact(config), nil
}

func (manager *Manager) Update(ctx context.Context, id string, incoming Config) (Config, error) {
	incoming.ID = strings.TrimSpace(id)
	manager.mu.Lock()
	if manager.disabled {
		manager.mu.Unlock()
		return Config{}, ErrDisabled
	}
	existing, index := manager.configByIDLocked(incoming.ID)
	manager.mu.Unlock()
	if index < 0 {
		return Config{}, ErrNotFound
	}
	if incoming.Password == "" || incoming.Password == PasswordMask {
		incoming.Password = existing.Password
	}
	if err := manager.prepareConfig(ctx, &incoming); err != nil {
		return Config{}, err
	}

	manager.mu.Lock()
	if manager.disabled {
		manager.mu.Unlock()
		return Config{}, ErrDisabled
	}
	existing, index = manager.configByIDLocked(incoming.ID)
	if index < 0 {
		manager.mu.Unlock()
		return Config{}, ErrNotFound
	}
	if err := manager.checkPortLocked(incoming, incoming.ID); err != nil {
		manager.mu.Unlock()
		return Config{}, err
	}
	wasRunning := manager.listeners[incoming.ID] != nil
	runtimeChanged := existing.Mode != incoming.Mode || existing.Interface != incoming.Interface ||
		existing.ListenHost != incoming.ListenHost || existing.ListenPort != incoming.ListenPort ||
		existing.AuthEnabled != incoming.AuthEnabled || existing.Username != incoming.Username || existing.Password != incoming.Password
	manager.configs[index] = incoming
	if err := manager.saveLocked(ctx); err != nil {
		manager.configs[index] = existing
		manager.mu.Unlock()
		return Config{}, err
	}
	manager.mu.Unlock()

	switch {
	case !incoming.Enabled:
		manager.stop(incoming.ID)
	case !wasRunning || runtimeChanged || !existing.Enabled:
		if err := manager.start(ctx, incoming.ID); err != nil {
			return redact(incoming), err
		}
	}
	return redact(incoming), nil
}

func (manager *Manager) Delete(ctx context.Context, id string) error {
	manager.mu.Lock()
	defer manager.mu.Unlock()
	if manager.disabled {
		return ErrDisabled
	}
	_, index := manager.configByIDLocked(strings.TrimSpace(id))
	if index < 0 {
		return ErrNotFound
	}
	manager.stopLocked(id)
	manager.configs = append(manager.configs[:index], manager.configs[index+1:]...)
	return manager.saveLocked(ctx)
}

// DeleteAllAndDisable is irreversible for the active developer-mode session:
// it closes every listener, removes every saved proxy, and rejects new work.
func (manager *Manager) DeleteAllAndDisable(ctx context.Context) error {
	manager.mu.Lock()
	for id := range manager.listeners {
		manager.stopLocked(id)
	}
	manager.configs = nil
	manager.disabled = true
	manager.mu.Unlock()
	err := manager.store.DeleteAppSetting(ctx, SettingKey)
	if errors.Is(err, store.ErrNotFound) {
		return nil
	}
	return err
}

func (manager *Manager) Close() error {
	manager.mu.Lock()
	defer manager.mu.Unlock()
	manager.disabled = true
	for id := range manager.listeners {
		manager.stopLocked(id)
	}
	return nil
}

func (manager *Manager) prepareConfig(ctx context.Context, config *Config) error {
	config.Name = strings.TrimSpace(config.Name)
	config.DeviceID = strings.TrimSpace(config.DeviceID)
	config.Interface = strings.TrimSpace(config.Interface)
	config.Mode = strings.ToLower(strings.TrimSpace(config.Mode))
	config.ListenHost = strings.TrimSpace(config.ListenHost)
	config.Username = strings.TrimSpace(config.Username)
	if config.Name == "" {
		config.Name = "proxy-" + config.ID[:4]
	}
	if config.DeviceID == "" {
		return errors.New("device is required")
	}
	device, err := manager.store.Device(ctx, config.DeviceID)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			return errors.New("configured device was not found")
		}
		return err
	}
	if strings.TrimSpace(device.Interface) == "" {
		return errors.New("the selected device has no cellular interface")
	}
	if config.Interface != "" && config.Interface != device.Interface {
		return errors.New("proxy interface does not match the selected device")
	}
	config.Interface = device.Interface
	if config.Enabled && !device.NetworkEnabled {
		return errors.New("enable roaming data on the selected device before starting its export proxy")
	}
	if config.Mode != "http" && config.Mode != "socks5" {
		return errors.New("mode must be http or socks5")
	}
	if config.ListenHost == "" {
		config.ListenHost = "0.0.0.0"
	}
	if net.ParseIP(config.ListenHost) == nil && config.ListenHost != "localhost" {
		return errors.New("listen host must be an IP address")
	}
	if config.ListenPort < 0 || config.ListenPort > 65535 {
		return errors.New("listen port must be between 0 and 65535")
	}
	if config.AuthEnabled {
		if config.Username == "" {
			return errors.New("username is required when authentication is enabled")
		}
		if len(config.Username) > 128 || len(config.Password) > 128 {
			return errors.New("proxy credentials are too long")
		}
	}
	return nil
}

func (manager *Manager) checkPortLocked(config Config, excludeID string) error {
	if config.ListenPort == 0 {
		return nil
	}
	for _, current := range manager.configs {
		if current.ID != excludeID && current.ListenPort == config.ListenPort && current.ListenHost == config.ListenHost {
			return fmt.Errorf("port %d is already used by another export proxy", config.ListenPort)
		}
	}
	if existing, _ := manager.configByIDLocked(excludeID); excludeID != "" &&
		existing.ListenHost == config.ListenHost && existing.ListenPort == config.ListenPort {
		return nil
	}
	listener, err := net.Listen("tcp", net.JoinHostPort(config.ListenHost, strconv.Itoa(config.ListenPort)))
	if err != nil {
		return fmt.Errorf("port %d is already in use", config.ListenPort)
	}
	_ = listener.Close()
	return nil
}

func (manager *Manager) start(ctx context.Context, id string) error {
	manager.mu.Lock()
	if manager.disabled {
		manager.mu.Unlock()
		return ErrDisabled
	}
	config, index := manager.configByIDLocked(id)
	if index < 0 || !config.Enabled {
		manager.mu.Unlock()
		return ErrNotFound
	}
	if err := platformSupported(); err != nil {
		manager.lastError[id] = err.Error()
		manager.mu.Unlock()
		return err
	}
	manager.stopLocked(id)
	listener, err := net.Listen("tcp", net.JoinHostPort(config.ListenHost, strconv.Itoa(config.ListenPort)))
	if err != nil {
		manager.lastError[id] = err.Error()
		manager.mu.Unlock()
		return err
	}
	if config.ListenPort == 0 {
		config.ListenPort = listener.Addr().(*net.TCPAddr).Port
		manager.configs[index] = config
		if err := manager.saveLocked(ctx); err != nil {
			_ = listener.Close()
			manager.mu.Unlock()
			return err
		}
	}
	delete(manager.lastError, id)
	manager.listeners[id] = listener
	manager.started[id] = time.Now().UTC()
	manager.mu.Unlock()
	go manager.serve(listener, config)
	return nil
}

func (manager *Manager) stop(id string) {
	manager.mu.Lock()
	defer manager.mu.Unlock()
	manager.stopLocked(id)
}

func (manager *Manager) stopLocked(id string) {
	if listener := manager.listeners[id]; listener != nil {
		_ = listener.Close()
		delete(manager.listeners, id)
	}
	delete(manager.started, id)
}

func (manager *Manager) serve(listener net.Listener, config Config) {
	dialer := boundDialer(config.Interface)
	resolver := boundResolver(config.Interface)
	for {
		connection, err := listener.Accept()
		if err != nil {
			return
		}
		go func(client net.Conn) {
			defer client.Close()
			var err error
			if config.Mode == "http" {
				err = serveHTTP(client, config, &dialer, resolver)
			} else {
				err = serveSOCKS(client, config, &dialer, resolver)
			}
			if err != nil {
				manager.logger.Debug("export proxy connection closed", "id", config.ID, "error", err)
			}
		}(connection)
	}
}

func (manager *Manager) configByIDLocked(id string) (Config, int) {
	for index, config := range manager.configs {
		if config.ID == id {
			return config, index
		}
	}
	return Config{}, -1
}

func redact(config Config) Config {
	if config.Password != "" {
		config.Password = PasswordMask
	}
	return config
}

func generateID() string {
	value := make([]byte, 4)
	_, _ = rand.Read(value)
	return hex.EncodeToString(value)
}
