package discovery

import (
	"context"
	"sync"
	"time"

	"github.com/eduard256/Strix/internal/models"
)

const (
	// ProbeTimeout is the overall timeout for all probes combined.
	ProbeTimeout = 3 * time.Second

	// ProbeTypeUnreachable indicates the device did not respond to ping.
	ProbeTypeUnreachable = "unreachable"
	// ProbeTypeStandard indicates a normal IP camera (RTSP/HTTP/ONVIF).
	ProbeTypeStandard = "standard"
	// ProbeTypeHomeKit indicates an Apple HomeKit camera that needs PIN pairing.
	ProbeTypeHomeKit = "homekit"
)

// Prober is an interface for network probe implementations.
// Each prober discovers specific information about a device at a given IP.
// New probers can be added by implementing this interface and registering
// them with ProbeService.
type Prober interface {
	// Name returns a unique identifier for this prober (e.g., "dns", "arp", "mdns").
	Name() string
	// Probe runs the probe against the given IP address.
	// Must respect context cancellation/timeout.
	// Returns nil result if nothing was found (not an error).
	Probe(ctx context.Context, ip string) (any, error)
}

// ProbeService orchestrates multiple probers to gather information about a device.
// It first pings the device, then runs all registered probers in parallel.
type ProbeService struct {
	pinger  *PingProber
	probers []Prober
	logger  interface {
		Debug(string, ...any)
		Error(string, error, ...any)
		Info(string, ...any)
	}
}

// NewProbeService creates a new ProbeService with the given probers.
// The ping prober is always included and runs first.
func NewProbeService(
	probers []Prober,
	logger interface {
		Debug(string, ...any)
		Error(string, error, ...any)
		Info(string, ...any)
	},
) *ProbeService {
	return &ProbeService{
		pinger:  &PingProber{},
		probers: probers,
		logger:  logger,
	}
}

// Probe runs ping + all registered probers against the given IP.
// Overall timeout is 3 seconds. Results are collected from whatever
// finishes in time; slow probers are omitted (nil in response).
func (s *ProbeService) Probe(ctx context.Context, ip string) *models.ProbeResponse {
	ctx, cancel := context.WithTimeout(ctx, ProbeTimeout)
	defer cancel()

	response := &models.ProbeResponse{
		IP:   ip,
		Type: ProbeTypeStandard,
	}

	// Step 1: Ping
	s.logger.Debug("probing device", "ip", ip)

	pingResult, err := s.pinger.Ping(ctx, ip)
	if err != nil || !pingResult.Reachable {
		errMsg := "device unreachable"
		if err != nil {
			errMsg = err.Error()
		}
		s.logger.Debug("ping failed", "ip", ip, "error", errMsg)
		response.Reachable = false
		response.Type = ProbeTypeUnreachable
		response.Error = errMsg
		return response
	}

	response.Reachable = true
	response.LatencyMs = pingResult.LatencyMs
	s.logger.Debug("ping OK", "ip", ip, "latency_ms", pingResult.LatencyMs)

	// Step 2: Run all probers in parallel
	type probeResult struct {
		name string
		data any
		err  error
	}

	results := make(chan probeResult, len(s.probers))
	var wg sync.WaitGroup

	for _, p := range s.probers {
		wg.Add(1)
		go func(prober Prober) {
			defer wg.Done()
			data, err := prober.Probe(ctx, ip)
			results <- probeResult{
				name: prober.Name(),
				data: data,
				err:  err,
			}
		}(p)
	}

	// Close results channel when all probers finish
	go func() {
		wg.Wait()
		close(results)
	}()

	// Collect results
	for r := range results {
		if r.err != nil {
			s.logger.Debug("prober failed", "prober", r.name, "error", r.err.Error())
			continue
		}
		if r.data == nil {
			continue
		}

		switch r.name {
		case "dns":
			if v, ok := r.data.(*models.DNSProbeResult); ok {
				response.Probes.DNS = v
			}
		case "arp":
			if v, ok := r.data.(*models.ARPProbeResult); ok {
				response.Probes.ARP = v
			}
		case "mdns":
			if v, ok := r.data.(*models.MDNSProbeResult); ok {
				response.Probes.MDNS = v
			}
		case "http":
			if v, ok := r.data.(*models.HTTPProbeResult); ok {
				response.Probes.HTTP = v
			}
		}
	}

	// Step 3: Determine type based on probe results
	response.Type = s.determineType(response)

	s.logger.Info("probe completed",
		"ip", ip,
		"reachable", response.Reachable,
		"type", response.Type,
		"latency_ms", response.LatencyMs,
	)

	return response
}

// determineType decides the device type based on collected probe results.
func (s *ProbeService) determineType(response *models.ProbeResponse) string {
	if !response.Reachable {
		return ProbeTypeUnreachable
	}

	// HomeKit camera that is not yet paired
	if response.Probes.MDNS != nil && !response.Probes.MDNS.Paired {
		category := response.Probes.MDNS.Category
		if category == "camera" || category == "doorbell" {
			return ProbeTypeHomeKit
		}
	}

	return ProbeTypeStandard
}
