package process

import (
	"context"
	"fmt"
	"net"
	"time"

	"github.com/safing/portbase/api"
	"github.com/safing/portbase/log"
	"github.com/safing/portmaster/service/network/netutils"
	"github.com/safing/portmaster/service/network/packet"
	"github.com/safing/portmaster/service/network/reference"
	"github.com/safing/portmaster/service/network/state"
	"github.com/safing/portmaster/service/profile"
)

// GetProcessWithProfile returns the process, including the profile.
// Always returns valid data.
// Errors are logged and returned for information or special handling purposes.
func GetProcessWithProfile(ctx context.Context, pid int) (process *Process, err error) {
	if !enableProcessDetection() {
		log.Tracer(ctx).Tracef("process: process detection disabled")
		return GetUnidentifiedProcess(ctx), nil
	}

	process, err = GetOrFindProcess(ctx, pid)
	if err != nil {
		log.Tracer(ctx).Debugf("process: failed to find process with PID: %s", err)
		return GetUnidentifiedProcess(ctx), err
	}

	// Get process group leader, which is the process "nearest" to the user and
	// will have more/better information for finding names ans icons, for example.
	err = process.FindProcessGroupLeader(ctx)
	if err != nil {
		log.Warningf("process: failed to get process group leader for %s: %s", process, err)
	}

	changed, err := process.GetProfile(ctx)
	if err != nil {
		log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)
	}

	if changed {
		process.Save()
	}

	return process, nil
}

// GetPidOfConnection returns the PID of the process that owns the described connection.
// Always returns valid data.
// Errors are logged and returned for information or special handling purposes.
func GetPidOfConnection(ctx context.Context, pktInfo *packet.Info) (pid int, connInbound bool, err error) {
	if !enableProcessDetection() {
		return UnidentifiedProcessID, pktInfo.Inbound, nil
	}

	// Use fast search for inbound packets, as the listening socket should
	// already be there for a while now.
	fastSearch := pktInfo.Inbound
	connInbound = pktInfo.Inbound

	// Check if we need to get the PID.
	if pktInfo.PID == UndefinedProcessID {
		log.Tracer(ctx).Tracef("process: getting pid from system network state")
		pid, connInbound, err = state.Lookup(pktInfo, fastSearch)
		if err != nil {
			err = fmt.Errorf("failed to find PID of connection: %w", err)
			log.Tracer(ctx).Tracef("process: %s", err)
			pid = UndefinedProcessID
		}
	} else {
		log.Tracer(ctx).Tracef("process: pid already set in packet (by ebpf or kext)")
		pid = pktInfo.PID
	}

	// Fallback to special profiles if PID could not be found.
	if pid == UndefinedProcessID {
		switch {
		case !connInbound:
			pid = UnidentifiedProcessID

		case netutils.ClassifyIP(pktInfo.Dst).IsLocalhost():
			// Always treat localhost connections as unidentified/unknown.
			pid = UnidentifiedProcessID

		case reference.IsICMP(uint8(pktInfo.Protocol)):
			// Always treat ICMP as unidentified/unknown, as the direction
			// might change to outgoing by new ICMP echo packets.
			pid = UnidentifiedProcessID

		default:
			// All other inbound connections are "unsolicited".
			pid = UnsolicitedProcessID
		}
	}

	return pid, connInbound, err
}

// GetNetworkHost returns a *Process that represents a host on the network.
func GetNetworkHost(ctx context.Context, remoteIP net.IP) (process *Process, err error) { //nolint:interfacer
	now := time.Now().Unix()
	networkHost := &Process{
		Name:      fmt.Sprintf("Device at %s", remoteIP),
		UserName:  "N/A",
		UserID:    NetworkHostProcessID,
		Pid:       NetworkHostProcessID,
		ParentPid: NetworkHostProcessID,
		Tags: []profile.Tag{
			{
				Key:   "ip",
				Value: remoteIP.String(),
			},
		},
		FirstSeen: now,
		LastSeen:  now,
	}

	// Get the (linked) local profile.
	networkHostProfile, err := profile.GetLocalProfile("", networkHost.MatchingData(), networkHost.CreateProfileCallback)
	if err != nil {
		return nil, err
	}

	// Assign profile to process.
	networkHost.PrimaryProfileID = networkHostProfile.ScopedID()
	networkHost.profile = networkHostProfile.LayeredProfile()

	return networkHost, nil
}

// GetProcessByRequestOrigin returns the process that initiated the API request ar.
func GetProcessByRequestOrigin(ar *api.Request) (*Process, error) {
	// get remote IP/Port
	remoteIP, remotePort, err := netutils.ParseIPPort(ar.RemoteAddr)
	if err != nil {
		return nil, fmt.Errorf("failed to get remote IP/Port: %w", err)
	}

	pkt := &packet.Info{
		Inbound:  false, // outbound as we are looking for the process of the source address
		Version:  packet.IPv4,
		Protocol: packet.TCP,
		Src:      remoteIP,   // source as in the process we are looking for
		SrcPort:  remotePort, // source as in the process we are looking for
		PID:      UndefinedProcessID,
	}

	pid, _, err := GetPidOfConnection(ar.Context(), pkt)
	if err != nil {
		return nil, err
	}

	proc, err := GetProcessWithProfile(ar.Context(), pid)
	if err != nil {
		return nil, err
	}

	return proc, nil
}