package navigator

import (
	"context"

	"github.com/safing/portmaster/base/log"
	"github.com/safing/portmaster/service/intel"
	"github.com/safing/portmaster/service/profile"
	"github.com/safing/portmaster/service/profile/endpoints"
	"github.com/safing/portmaster/spn/hub"
)

// HubType is the usage type of a Hub in routing.
type HubType uint8

// Hub Types.
const (
	HomeHub HubType = iota
	TransitHub
	DestinationHub
)

// DeriveTunnelOptions derives and returns the tunnel options from the connection and profile.
// This function lives in firewall/tunnel.go and is set here to avoid import loops.
var DeriveTunnelOptions func(lp *profile.LayeredProfile, destination *intel.Entity, connEncrypted bool) *Options

// Options holds configuration options for operations with the Map.
type Options struct { //nolint:maligned
	// Home holds the options for Home Hubs.
	Home *HomeHubOptions

	// Transit holds the options for Transit Hubs.
	Transit *TransitHubOptions

	// Destination holds the options for Destination Hubs.
	Destination *DestinationHubOptions

	// RoutingProfile defines the algorithm to use to find a route.
	RoutingProfile string
}

// HomeHubOptions holds configuration options for Home Hub operations with the Map.
type HomeHubOptions HubOptions

// TransitHubOptions holds configuration options for Transit Hub operations with the Map.
type TransitHubOptions HubOptions

// DestinationHubOptions holds configuration options for Destination Hub operations with the Map.
type DestinationHubOptions HubOptions

// HubOptions holds configuration options for a specific hub type for operations with the Map.
type HubOptions struct {
	// Regard holds required States. Only Hubs where all of these are present
	// will taken into account for the operation. If NoDefaults is not set, a
	// basic set of desirable states is added automatically.
	Regard PinState

	// Disregard holds disqualifying States. Only Hubs where none of these are
	// present will be taken into account for the operation. If NoDefaults is not
	// set, a basic set of undesirable states is added automatically.
	Disregard PinState

	// NoDefaults declares whether default and recommended Regard and Disregard states should not be used.
	NoDefaults bool

	// HubPolicies is a collection of endpoint lists that Hubs must pass in order
	// to be taken into account for the operation.
	HubPolicies []endpoints.Endpoints

	// RequireVerifiedOwners specifies which verified owners are allowed to be used.
	// If the list is empty, all owners are allowed.
	RequireVerifiedOwners []string

	// CheckHubPolicyWith provides an entity that must match the Hubs entry or exit
	// policy (depending on type) in order to be taken into account for the operation.
	CheckHubPolicyWith *intel.Entity
}

// Copy returns a shallow copy of the Options.
func (o *Options) Copy() *Options {
	copied := &Options{
		RoutingProfile: o.RoutingProfile,
	}
	if o.Home != nil {
		c := HomeHubOptions(HubOptions(*o.Home).Copy())
		copied.Home = &c
	}
	if o.Transit != nil {
		c := TransitHubOptions(HubOptions(*o.Transit).Copy())
		copied.Transit = &c
	}
	if o.Destination != nil {
		c := DestinationHubOptions(HubOptions(*o.Destination).Copy())
		copied.Destination = &c
	}
	return copied
}

// Copy returns a shallow copy of the Options.
func (o HubOptions) Copy() HubOptions {
	return HubOptions{
		Regard:                o.Regard,
		Disregard:             o.Disregard,
		NoDefaults:            o.NoDefaults,
		HubPolicies:           o.HubPolicies,
		RequireVerifiedOwners: o.RequireVerifiedOwners,
		CheckHubPolicyWith:    o.CheckHubPolicyWith,
	}
}

// PinMatcher is a stateful matching function generated by Options.
type PinMatcher func(pin *Pin) bool

// DefaultOptions returns the default options for this Map.
func (m *Map) DefaultOptions() *Options {
	m.Lock()
	defer m.Unlock()

	return m.defaultOptions()
}

func (m *Map) defaultOptions() *Options {
	opts := &Options{
		RoutingProfile: DefaultRoutingProfileID,
	}

	return opts
}

// HubPoliciesAreSet returns whether any of the given hub policies are set and non-empty.
func HubPoliciesAreSet(policies []endpoints.Endpoints) bool {
	for _, policy := range policies {
		if policy.IsSet() {
			return true
		}
	}
	return false
}

var emptyHubOptions = &HubOptions{}

// Matcher generates a PinMatcher based on the Options.
func (o *HomeHubOptions) Matcher(hubIntel *hub.Intel) PinMatcher {
	if o == nil {
		return emptyHubOptions.Matcher(HomeHub, hubIntel)
	}

	// Convert and call base func.
	ho := HubOptions(*o)
	return ho.Matcher(HomeHub, hubIntel)
}

// Matcher generates a PinMatcher based on the Options.
func (o *TransitHubOptions) Matcher(hubIntel *hub.Intel) PinMatcher {
	if o == nil {
		return emptyHubOptions.Matcher(TransitHub, hubIntel)
	}

	// Convert and call base func.
	ho := HubOptions(*o)
	return ho.Matcher(TransitHub, hubIntel)
}

// Matcher generates a PinMatcher based on the Options.
func (o *DestinationHubOptions) Matcher(hubIntel *hub.Intel) PinMatcher {
	if o == nil {
		return emptyHubOptions.Matcher(DestinationHub, hubIntel)
	}

	// Convert and call base func.
	ho := HubOptions(*o)
	return ho.Matcher(DestinationHub, hubIntel)
}

// Matcher generates a PinMatcher based on the Options.
// Always use the Matcher on option structs if you can.
func (o *Options) Matcher(hubType HubType, hubIntel *hub.Intel) PinMatcher {
	switch hubType {
	case HomeHub:
		return o.Home.Matcher(hubIntel)
	case TransitHub:
		return o.Transit.Matcher(hubIntel)
	case DestinationHub:
		return o.Destination.Matcher(hubIntel)
	default:
		return nil // This will panic, but should never be used.
	}
}

// Matcher generates a PinMatcher based on the Options.
func (o *HubOptions) Matcher(hubType HubType, hubIntel *hub.Intel) PinMatcher {
	// Fallback to empty hub options.
	if o == nil {
		o = emptyHubOptions
	}

	// Compile states to regard and disregard.
	regard := o.Regard
	disregard := o.Disregard

	// Add default states.
	if !o.NoDefaults {
		// Add default States.
		regard = regard.Add(StateSummaryRegard)
		disregard = disregard.Add(StateSummaryDisregard)

		// Add type based Advisories.
		switch hubType {
		case HomeHub:
			// Home Hubs don't need to be reachable and don't need keys ready to be used.
			regard = regard.Remove(StateReachable)
			regard = regard.Remove(StateActive)
			// Follow advisory.
			disregard = disregard.Add(StateUsageAsHomeDiscouraged)
			// Home Hub may be the current Home Hub.
			disregard = disregard.Remove(StateIsHomeHub)
		case TransitHub:
			// Transit Hubs get no additional states.
		case DestinationHub:
			// Follow advisory.
			disregard = disregard.Add(StateUsageAsDestinationDiscouraged)
			// Do not use if Hub reports network issues.
			disregard = disregard.Add(StateConnectivityIssues)
		}
	}

	// Add intel policies.
	hubPolicies := o.HubPolicies
	if hubIntel != nil && hubIntel.Parsed() != nil {
		switch hubType {
		case HomeHub:
			hubPolicies = append(hubPolicies, hubIntel.Parsed().HubAdvisory, hubIntel.Parsed().HomeHubAdvisory)
		case TransitHub:
			hubPolicies = append(hubPolicies, hubIntel.Parsed().HubAdvisory)
		case DestinationHub:
			hubPolicies = append(hubPolicies, hubIntel.Parsed().HubAdvisory, hubIntel.Parsed().DestinationHubAdvisory)
		}
	}

	// Add entry/exit policiy checks.
	checkHubPolicyWith := o.CheckHubPolicyWith

	return func(pin *Pin) bool {
		// Check required Pin States.
		if !pin.State.Has(regard) || pin.State.HasAnyOf(disregard) {
			return false
		}

		// Check if all required states from intel were applied.
		if regard.HasAnyOf(StateSummaryStatusesAppliedFromIntel) || disregard.HasAnyOf(StateSummaryStatusesAppliedFromIntel) {
			if pin.stateIntelApplied.IsNotSet() {
				log.Warningf("spn/navigator: pin %s skipped as intel statuses were not applied", pin.Hub.ID)
				return false
			}
		}

		// Check verified owners.
		if len(o.RequireVerifiedOwners) > 0 {
			// Check if Pin has a verified owner at all.
			if pin.VerifiedOwner == "" {
				return false
			}

			// Check if verified owner is in the list.
			inList := false
			for _, allowed := range o.RequireVerifiedOwners {
				if pin.VerifiedOwner == allowed {
					inList = true
					break
				}
			}

			// Pin does not have a verified owner from the allowed list.
			if !inList {
				return false
			}
		}

		// Check policies.
	policyCheck:
		for _, policy := range hubPolicies {
			// Check if policy is set.
			if !policy.IsSet() {
				continue
			}

			// Check if policy matches.
			result, reason := policy.MatchMulti(context.TODO(), pin.EntityV4, pin.EntityV6)
			switch result {
			case endpoints.NoMatch:
				// Continue with check.
			case endpoints.MatchError:
				log.Warningf("spn/navigator: failed to match policy: %s", reason)
				// Continue with check for now.
				// TODO: Rethink how to do this. If eg. the geoip database has a
				// problem, then no Hub will match. For now, just continue to the
				// next rule set. Not optimal, but fail safe.
			case endpoints.Denied:
				// Explicitly denied, abort immediately.
				return false
			case endpoints.Permitted:
				// Explicitly allowed, abort check and continue.
				break policyCheck
			}
		}

		// Check entry/exit policies.
		if checkHubPolicyWith != nil {
			switch hubType {
			case HomeHub:
				if endpointListMatch(pin.Hub.Info.EntryPolicy(), checkHubPolicyWith) == endpoints.Denied {
					// Hub does not allow entry from the given entity.
					return false
				}
			case TransitHub:
				// Transit Hubs do not have a hub policy.
			case DestinationHub:
				if endpointListMatch(pin.Hub.Info.ExitPolicy(), checkHubPolicyWith) == endpoints.Denied {
					// Hub does not allow exit to the given entity.
					return false
				}
			}
		}

		return true // All checks have passed.
	}
}

func endpointListMatch(list endpoints.Endpoints, entity *intel.Entity) endpoints.EPResult {
	// Check if endpoint list and entity are available.
	if !list.IsSet() || entity == nil {
		return endpoints.NoMatch
	}

	// Match and return result only.
	result, _ := list.Match(context.TODO(), entity)
	return result
}
