package firewall

import (
	"errors"
	"flag"
	"fmt"
	"path/filepath"
	"strings"
	"sync/atomic"
	"time"

	"github.com/safing/portmaster/base/config"
	"github.com/safing/portmaster/base/log"
	_ "github.com/safing/portmaster/service/core"
	"github.com/safing/portmaster/service/mgr"
	"github.com/safing/portmaster/service/netquery"
	"github.com/safing/portmaster/service/network"
	"github.com/safing/portmaster/service/profile"
	"github.com/safing/portmaster/service/resolver"
	"github.com/safing/portmaster/service/updates"
	"github.com/safing/portmaster/spn/access"
	"github.com/safing/portmaster/spn/captain"
)

type stringSliceFlag []string

func (ss *stringSliceFlag) String() string {
	return strings.Join(*ss, ":")
}

func (ss *stringSliceFlag) Set(value string) error {
	*ss = append(*ss, filepath.Clean(value))
	return nil
}

var allowedClients stringSliceFlag

type Firewall struct {
	mgr      *mgr.Manager
	instance instance
}

func init() {
	flag.Var(&allowedClients, "allowed-clients", "A list of binaries that are allowed to connect to the Portmaster API")
}

func (f *Firewall) Manager() *mgr.Manager {
	return f.mgr
}

func (f *Firewall) Start() error {
	if err := prep(); err != nil {
		log.Errorf("Failed to prepare firewall module %q", err)
		return err
	}

	return start()
}

func (f *Firewall) Stop() error {
	// Cancel all workers and give them a little time.
	// The bandwidth updater can crash the sqlite DB for some reason.
	// TODO: Investigate.
	f.mgr.Cancel()
	time.Sleep(100 * time.Millisecond)

	return stop()
}

func prep() error {
	network.SetDefaultFirewallHandler(defaultFirewallHandler)

	// Reset connections every time configuration changes
	// this will be triggered on spn enable/disable
	module.instance.Config().EventConfigChange.AddCallback("reset connection verdicts after global config change", func(w *mgr.WorkerCtx, _ struct{}) (bool, error) {
		resetAllConnectionVerdicts()
		return false, nil
	})

	module.instance.Profile().EventConfigChange.AddCallback("reset connection verdicts after profile config change",
		func(m *mgr.WorkerCtx, profileID string) (bool, error) {
			// Expected event data: scoped profile ID.
			profileSource, profileID, ok := strings.Cut(profileID, "/")
			if !ok {
				return false, fmt.Errorf("event data does not seem to be a scoped profile ID: %v", profileID)
			}

			resetProfileConnectionVerdict(profileSource, profileID)
			return false, nil
		},
	)

	// Reset connections when spn is connected
	// connect and disconnecting is triggered on config change event but connecting takеs more time
	module.instance.Captain().EventSPNConnected.AddCallback("reset connection verdicts on SPN connect", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
		resetAllConnectionVerdicts()
		return false, err
	})

	// Reset connections when account is updated.
	// This will not change verdicts, but will update the feature flags on connections.
	module.instance.Access().EventAccountUpdate.AddCallback("update connection feature flags after account update", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
		resetAllConnectionVerdicts()
		return false, err
	})

	module.instance.Network().EventConnectionReattributed.AddCallback("reset connection verdicts after connection re-attribution", func(wc *mgr.WorkerCtx, connID string) (cancel bool, err error) {
		// Expected event data: connection ID.
		resetSingleConnectionVerdict(connID)
		return false, err
	})

	return nil
}

func start() error {
	getConfig()
	startAPIAuth()

	module.mgr.Go("packet handler", packetHandler)
	module.mgr.Go("bandwidth update handler", bandwidthUpdateHandler)

	// Start stat logger if logging is set to trace.
	if log.GetLogLevel() == log.TraceLevel {
		module.mgr.Go("stat logger", statLogger)
	}

	return nil
}

func stop() error {
	return nil
}

var (
	module     *Firewall
	shimLoaded atomic.Bool
)

func New(instance instance) (*Firewall, error) {
	if !shimLoaded.CompareAndSwap(false, true) {
		return nil, errors.New("only one instance allowed")
	}

	m := mgr.New("Firewall")
	module = &Firewall{
		mgr:      m,
		instance: instance,
	}

	if err := prepAPIAuth(); err != nil {
		return nil, err
	}

	if err := registerConfig(); err != nil {
		return nil, err
	}

	return module, nil
}

type instance interface {
	BinDir() string
	Config() *config.Config
	BinaryUpdates() *updates.Updater
	Profile() *profile.ProfileModule
	Captain() *captain.Captain
	Access() *access.Access
	Network() *network.Network
	NetQuery() *netquery.NetQuery
	Resolver() *resolver.ResolverModule
}
