package navigator

import (
	"context"
	"errors"

	"golang.org/x/exp/slices"

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

// UpdateIntel supplies the map with new intel data. The data is not copied, so
// it must not be modified after being supplied. If the map is empty, the
// bootstrap hubs will be added to the map.
func (m *Map) UpdateIntel(update *hub.Intel, trustNodes []string) error {
	// Check if intel data is already parsed.
	if update.Parsed() == nil {
		return errors.New("intel data is not parsed")
	}

	m.Lock()
	defer m.Unlock()

	// Update the map's reference to the intel data.
	m.intel = update

	// Update pins with new intel data.
	for _, pin := range m.all {
		// Add/Update location data from IP addresses.
		pin.updateLocationData()

		// Override Pin Data.
		m.updateInfoOverrides(pin)

		// Update Trust and Advisory Statuses.
		m.updateIntelStatuses(pin, trustNodes)

		// Push changes.
		// TODO: Only set when pin changed.
		pin.pushChanges.Set()
	}

	// Configure the map's regions.
	m.updateRegions(m.intel.Regions)

	// Push pin changes.
	m.PushPinChanges()

	log.Infof("spn/navigator: updated intel on map %s", m.Name)

	// Add bootstrap hubs if map is empty.
	if m.isEmpty() {
		return m.addBootstrapHubs(m.intel.BootstrapHubs)
	}
	return nil
}

// GetIntel returns the map's intel data.
func (m *Map) GetIntel() *hub.Intel {
	m.RLock()
	defer m.RUnlock()

	return m.intel
}

func (m *Map) updateIntelStatuses(pin *Pin, trustNodes []string) {
	// Reset all related states (StateSummaryStatusesAppliedFromIntel).
	pin.stateIntelApplied.UnSet()
	pin.removeStates(StateTrusted | StateUsageDiscouraged | StateUsageAsHomeDiscouraged | StateUsageAsDestinationDiscouraged) // same as: StateSummaryStatusesAppliedFromIntel

	// Check if Intel data is loaded.
	if m.intel == nil {
		return
	}

	// Indicate that intel statuses have been applied to the pin
	defer pin.stateIntelApplied.Set()

	// Check Hub Intel
	hubIntel, ok := m.intel.Hubs[pin.Hub.ID]
	if ok {
		// Apply the verified owner, if any.
		pin.VerifiedOwner = hubIntel.VerifiedOwner

		// Check if Hub is discontinued.
		if hubIntel.Discontinued {
			// Reset state, set offline and return.
			pin.State = StateNone
			pin.addStates(StateOffline)
			return
		}

		// Check if Hub is trusted.
		if hubIntel.Trusted {
			pin.addStates(StateTrusted)
		}
	}

	// Check manual trust status.
	switch {
	case slices.Contains[[]string, string](trustNodes, pin.VerifiedOwner):
		pin.addStates(StateTrusted)
	case slices.Contains[[]string, string](trustNodes, pin.Hub.ID):
		pin.addStates(StateTrusted)
	}

	// Check advisories.
	// Check for UsageDiscouraged.
	checkStatusList(
		pin,
		StateUsageDiscouraged,
		m.intel.AdviseOnlyTrustedHubs,
		m.intel.Parsed().HubAdvisory,
	)
	// Check for UsageAsHomeDiscouraged.
	checkStatusList(
		pin,
		StateUsageAsHomeDiscouraged,
		m.intel.AdviseOnlyTrustedHomeHubs,
		m.intel.Parsed().HomeHubAdvisory,
	)
	// Check for UsageAsDestinationDiscouraged.
	checkStatusList(
		pin,
		StateUsageAsDestinationDiscouraged,
		m.intel.AdviseOnlyTrustedDestinationHubs,
		m.intel.Parsed().DestinationHubAdvisory,
	)
}

func checkStatusList(pin *Pin, state PinState, requireTrusted bool, endpointList endpoints.Endpoints) {
	if requireTrusted && !pin.State.Has(StateTrusted) {
		pin.addStates(state)
		return
	}

	if pin.EntityV4 != nil {
		result, _ := endpointList.Match(context.TODO(), pin.EntityV4)
		if result == endpoints.Denied {
			pin.addStates(state)
			return
		}
	}

	if pin.EntityV6 != nil {
		result, _ := endpointList.Match(context.TODO(), pin.EntityV6)
		if result == endpoints.Denied {
			pin.addStates(state)
		}
	}
}

func (m *Map) updateInfoOverrides(pin *Pin) {
	// Check if Intel data is loaded and if there are any overrides.
	if m.intel == nil {
		return
	}

	// Get overrides for this pin.
	hubIntel, ok := m.intel.Hubs[pin.Hub.ID]
	if !ok || hubIntel.Override == nil {
		return
	}
	overrides := hubIntel.Override

	// Apply overrides
	if overrides.CountryCode != "" {
		if pin.LocationV4 != nil {
			pin.LocationV4.Country = geoip.GetCountryInfo(overrides.CountryCode)
		}
		if pin.EntityV4 != nil {
			pin.EntityV4.Country = overrides.CountryCode
		}
		if pin.LocationV6 != nil {
			pin.LocationV6.Country = geoip.GetCountryInfo(overrides.CountryCode)
		}
		if pin.EntityV6 != nil {
			pin.EntityV6.Country = overrides.CountryCode
		}
	}
	if overrides.Coordinates != nil {
		if pin.LocationV4 != nil {
			pin.LocationV4.Coordinates = *overrides.Coordinates
		}
		if pin.EntityV4 != nil {
			pin.EntityV4.Coordinates = overrides.Coordinates
		}
		if pin.LocationV6 != nil {
			pin.LocationV6.Coordinates = *overrides.Coordinates
		}
		if pin.EntityV6 != nil {
			pin.EntityV6.Coordinates = overrides.Coordinates
		}
	}
	if overrides.ASN != 0 {
		if pin.LocationV4 != nil {
			pin.LocationV4.AutonomousSystemNumber = overrides.ASN
		}
		if pin.EntityV4 != nil {
			pin.EntityV4.ASN = overrides.ASN
		}
		if pin.LocationV6 != nil {
			pin.LocationV6.AutonomousSystemNumber = overrides.ASN
		}
		if pin.EntityV6 != nil {
			pin.EntityV6.ASN = overrides.ASN
		}
	}
	if overrides.ASOrg != "" {
		if pin.LocationV4 != nil {
			pin.LocationV4.AutonomousSystemOrganization = overrides.ASOrg
		}
		if pin.EntityV4 != nil {
			pin.EntityV4.ASOrg = overrides.ASOrg
		}
		if pin.LocationV6 != nil {
			pin.LocationV6.AutonomousSystemOrganization = overrides.ASOrg
		}
		if pin.EntityV6 != nil {
			pin.EntityV6.ASOrg = overrides.ASOrg
		}
	}
}
