package huebridge

import (
	"context"
	"crypto/tls"
	"fmt"
	"maps"
	"math"
	"net/http"
	"net/url"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/tdrn-org/go-hue"

	"github.com/influxdata/telegraf"
)

type bridge struct {
	url                   *url.URL
	configRoomAssignments map[string]string
	remoteCfg             *remoteClientConfig
	tlsCfg                *tls.Config
	timeout               time.Duration
	log                   telegraf.Logger

	resolvedClient  hue.BridgeClient
	resourceTree    map[string]string
	deviceNames     map[string]string
	roomAssignments map[string]string
}

func (b *bridge) String() string {
	return b.url.Redacted()
}

func (b *bridge) process(ctx context.Context, acc telegraf.Accumulator) error {
	if b.resolvedClient == nil {
		if err := b.resolve(); err != nil {
			return err
		}
	}
	b.log.Tracef("Processing bridge %s", b)
	if err := b.fetchMetadata(ctx); err != nil {
		// Discard previously resolved client and re-resolve on next process call
		b.resolvedClient = nil
		return err
	}
	acc.AddError(b.processLights(ctx, acc))
	acc.AddError(b.processTemperatures(ctx, acc))
	acc.AddError(b.processLightLevels(ctx, acc))
	acc.AddError(b.processMotionSensors(ctx, acc))
	acc.AddError(b.processDevicePowers(ctx, acc))
	return nil
}

func (b *bridge) processLights(ctx context.Context, acc telegraf.Accumulator) error {
	getLightsResponse, err := b.resolvedClient.GetLights(ctx)
	if err != nil {
		return fmt.Errorf("failed to access bridge lights on %s: %w", b, err)
	}
	if getLightsResponse.HTTPResponse.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to fetch bridge lights from %s: %s", b, getLightsResponse.HTTPResponse.Status)
	}
	responseData := getLightsResponse.JSON200.Data
	for _, light := range responseData {
		tags := map[string]string{
			"bridge_id": b.resolvedClient.Bridge().BridgeId,
			"room":      b.resolveResourceRoom(light.Id, light.Metadata.Name),
			"device":    light.Metadata.Name,
		}
		fields := make(map[string]interface{}, 5)
		if light.On.On {
			fields["on"] = 1
		} else {
			fields["on"] = 0
		}
		if light.Dimming != nil && light.Dimming.Brightness != nil {
			fields["brightness"] = float64(*light.Dimming.Brightness)
		}
		if light.ColorTemperature != nil &&
			light.ColorTemperature.MirekValid != nil && *light.ColorTemperature.MirekValid &&
			light.ColorTemperature.Mirek != nil {
			fields["color_temp"] = int64(*light.ColorTemperature.Mirek)
		}
		if light.Color != nil && light.Color.Xy != nil {
			fields["color_x"] = float64(light.Color.Xy.X)
			fields["color_y"] = float64(light.Color.Xy.Y)
		}
		acc.AddGauge("huebridge_light", fields, tags)
	}
	return nil
}

func (b *bridge) processTemperatures(ctx context.Context, acc telegraf.Accumulator) error {
	getTemperaturesResponse, err := b.resolvedClient.GetTemperatures(ctx)
	if err != nil {
		return fmt.Errorf("failed to access bridge temperatures on %s: %w", b, err)
	}
	if getTemperaturesResponse.HTTPResponse.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to fetch bridge temperatures from %s: %s", b, getTemperaturesResponse.HTTPResponse.Status)
	}
	responseData := getTemperaturesResponse.JSON200.Data
	for _, temperature := range responseData {
		temperatureName := b.resolveDeviceName(temperature.Id)
		tags := map[string]string{
			"bridge_id": b.resolvedClient.Bridge().BridgeId,
			"room":      b.resolveResourceRoom(temperature.Id, temperatureName),
			"device":    temperatureName,
			"enabled":   strconv.FormatBool(temperature.Enabled),
		}
		fields := map[string]interface{}{
			"temperature": *temperature.Temperature.TemperatureReport.Temperature,
		}
		acc.AddGauge("huebridge_temperature", fields, tags)
	}
	return nil
}

func (b *bridge) processLightLevels(ctx context.Context, acc telegraf.Accumulator) error {
	getLightLevelsResponse, err := b.resolvedClient.GetLightLevels(ctx)
	if err != nil {
		return fmt.Errorf("failed to access bridge lights levels on %s: %w", b, err)
	}
	if getLightLevelsResponse.HTTPResponse.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to fetch bridge light levels from %s: %s", b, getLightLevelsResponse.HTTPResponse.Status)
	}
	responseData := getLightLevelsResponse.JSON200.Data
	for _, lightLevel := range responseData {
		lightLevelName := b.resolveDeviceName(lightLevel.Id)
		tags := map[string]string{
			"bridge_id": b.resolvedClient.Bridge().BridgeId,
			"room":      b.resolveResourceRoom(lightLevel.Id, lightLevelName),
			"device":    lightLevelName,
			"enabled":   strconv.FormatBool(lightLevel.Enabled),
		}
		fields := map[string]interface{}{
			"light_level":     *lightLevel.Light.LightLevelReport.LightLevel,
			"light_level_lux": math.Pow(10.0, (float64(*lightLevel.Light.LightLevelReport.LightLevel)-1.0)/10000.0),
		}
		acc.AddGauge("huebridge_light_level", fields, tags)
	}
	return nil
}

func (b *bridge) processMotionSensors(ctx context.Context, acc telegraf.Accumulator) error {
	getMotionSensorsResponse, err := b.resolvedClient.GetMotionSensors(ctx)
	if err != nil {
		return fmt.Errorf("failed to access bridge motion sensors on %s: %w", b, err)
	}
	if getMotionSensorsResponse.HTTPResponse.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to fetch bridge motion sensors from %s: %s", b, getMotionSensorsResponse.HTTPResponse.Status)
	}
	responseData := getMotionSensorsResponse.JSON200.Data
	for _, motionSensor := range responseData {
		motionSensorName := b.resolveDeviceName(motionSensor.Id)
		tags := map[string]string{
			"bridge_id": b.resolvedClient.Bridge().BridgeId,
			"room":      b.resolveResourceRoom(motionSensor.Id, motionSensorName),
			"device":    motionSensorName,
			"enabled":   strconv.FormatBool(motionSensor.Enabled),
		}
		fields := make(map[string]interface{}, 1)
		if *motionSensor.Motion.MotionReport.Motion {
			fields["motion"] = 1
		} else {
			fields["motion"] = 0
		}
		acc.AddGauge("huebridge_motion_sensor", fields, tags)
	}
	return nil
}

func (b *bridge) processDevicePowers(ctx context.Context, acc telegraf.Accumulator) error {
	getDevicePowersResponse, err := b.resolvedClient.GetDevicePowers(ctx)
	if err != nil {
		return fmt.Errorf("failed to access bridge device powers on %s: %w", b, err)
	}
	if getDevicePowersResponse.HTTPResponse.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to fetch bridge device powers from %s: %s", b, getDevicePowersResponse.HTTPResponse.Status)
	}
	responseData := getDevicePowersResponse.JSON200.Data
	for _, devicePower := range responseData {
		if devicePower.PowerState.BatteryLevel == nil && devicePower.PowerState.BatteryState == nil {
			continue
		}
		devicePowerName := b.resolveDeviceName(devicePower.Id)
		tags := map[string]string{
			"bridge_id": b.resolvedClient.Bridge().BridgeId,
			"room":      b.resolveResourceRoom(devicePower.Id, devicePowerName),
			"device":    devicePowerName,
		}
		fields := map[string]interface{}{
			"battery_level": *devicePower.PowerState.BatteryLevel,
			"battery_state": *devicePower.PowerState.BatteryState,
		}
		acc.AddGauge("huebridge_device_power", fields, tags)
	}
	return nil
}

func (b *bridge) resolve() error {
	if b.resolvedClient != nil {
		return nil
	}
	switch b.url.Scheme {
	case "address":
		return b.resolveViaAddress()
	case "cloud":
		return b.resolveViaCloud()
	case "mdns":
		return b.resolveViaMDNS()
	case "remote":
		return b.resolveViaRemote()
	}
	return fmt.Errorf("unrecognized bridge URL %s", b)
}

func (b *bridge) resolveViaAddress() error {
	locator, err := hue.NewAddressBridgeLocator(b.url.Host)
	if err != nil {
		return err
	}
	return b.resolveLocalBridge(locator)
}

func (b *bridge) resolveViaCloud() error {
	locator := hue.NewCloudBridgeLocator()
	if b.url.Host != "" {
		u, err := url.Parse(fmt.Sprintf("https://%s/", b.url.Host))
		if err != nil {
			return err
		}
		locator.DiscoveryEndpointUrl = u.JoinPath(b.url.Path)
	}
	locator.TlsConfig = b.tlsCfg
	return b.resolveLocalBridge(locator)
}

func (b *bridge) resolveViaMDNS() error {
	locator := hue.NewMDNSBridgeLocator()
	return b.resolveLocalBridge(locator)
}

func (b *bridge) resolveLocalBridge(locator hue.BridgeLocator) error {
	hueBridge, err := locator.Lookup(b.url.User.Username(), b.timeout)
	if err != nil {
		return err
	}
	urlPassword, _ := b.url.User.Password()
	bridgeClient, err := hueBridge.NewClient(hue.NewLocalBridgeAuthenticator(urlPassword), b.timeout)
	if err != nil {
		return err
	}
	b.resolvedClient = bridgeClient
	return nil
}

func (b *bridge) resolveViaRemote() error {
	var redirectURL *url.URL
	if b.remoteCfg.RemoteCallbackURL != "" {
		u, err := url.Parse(b.remoteCfg.RemoteCallbackURL)
		if err != nil {
			return err
		}
		redirectURL = u
	}
	tokenFile := filepath.Join(
		b.remoteCfg.RemoteTokenDir,
		b.remoteCfg.RemoteClientID,
		strings.ToUpper(b.url.User.Username())+".json",
	)
	locator, err := hue.NewRemoteBridgeLocator(
		b.remoteCfg.RemoteClientID,
		b.remoteCfg.RemoteClientSecret,
		redirectURL,
		tokenFile,
	)
	if err != nil {
		return err
	}
	if b.url.Host != "" {
		u, err := url.Parse(fmt.Sprintf("https://%s/", b.url.Host))
		if err != nil {
			return err
		}
		locator.EndpointUrl = u.JoinPath(b.url.Path)
	}
	locator.TlsConfig = b.tlsCfg
	return b.resolveRemoteBridge(locator)
}

func (b *bridge) resolveRemoteBridge(locator *hue.RemoteBridgeLocator) error {
	hueBridge, err := locator.Lookup(b.url.User.Username(), b.timeout)
	if err != nil {
		return err
	}
	urlPassword, _ := b.url.User.Password()
	bridgeClient, err := hueBridge.NewClient(hue.NewRemoteBridgeAuthenticator(locator, urlPassword), b.timeout)
	if err != nil {
		return err
	}
	b.resolvedClient = bridgeClient
	return nil
}

func (b *bridge) fetchMetadata(ctx context.Context) error {
	err := b.fetchResourceTree(ctx)
	if err != nil {
		return err
	}
	err = b.fetchDeviceNames(ctx)
	if err != nil {
		return err
	}
	return b.fetchRoomAssignments(ctx)
}

func (b *bridge) fetchResourceTree(ctx context.Context) error {
	getResourcesResponse, err := b.resolvedClient.GetResources(ctx)
	if err != nil {
		return fmt.Errorf("failed to access bridge resources on %s: %w", b, err)
	}
	if getResourcesResponse.HTTPResponse.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to fetch bridge resources from %s: %s", b, getResourcesResponse.HTTPResponse.Status)
	}
	responseData := getResourcesResponse.JSON200.Data
	if responseData == nil {
		b.resourceTree = make(map[string]string)
		return nil
	}
	b.resourceTree = make(map[string]string, len(*responseData))
	for _, resource := range *responseData {
		b.resourceTree[resource.Id] = resource.Owner.Rid
	}
	return nil
}

func (b *bridge) fetchDeviceNames(ctx context.Context) error {
	getDevicesResponse, err := b.resolvedClient.GetDevices(ctx)
	if err != nil {
		return fmt.Errorf("failed to access bridge devices on %s: %w", b, err)
	}
	if getDevicesResponse.HTTPResponse.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to fetch bridge devices from %s: %s", b, getDevicesResponse.HTTPResponse.Status)
	}
	responseData := getDevicesResponse.JSON200.Data
	b.deviceNames = make(map[string]string, len(responseData))
	for _, device := range responseData {
		b.deviceNames[device.Id] = device.Metadata.Name
	}
	return nil
}

func (b *bridge) fetchRoomAssignments(ctx context.Context) error {
	getRoomsResponse, err := b.resolvedClient.GetRooms(ctx)
	if err != nil {
		return fmt.Errorf("failed to access bridge rooms on %s: %w", b, err)
	}
	if getRoomsResponse.HTTPResponse.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to fetch bridge rooms from %s: %s", b, getRoomsResponse.HTTPResponse.Status)
	}
	responseData := getRoomsResponse.JSON200.Data
	b.roomAssignments = make(map[string]string, len(responseData))
	for _, roomGet := range responseData {
		for _, children := range roomGet.Children {
			b.roomAssignments[children.Rid] = roomGet.Metadata.Name
		}
	}
	maps.Copy(b.roomAssignments, b.configRoomAssignments)
	return nil
}

func (b *bridge) resolveResourceRoom(resourceID, resourceName string) string {
	roomName := b.roomAssignments[resourceName]
	if roomName != "" {
		return roomName
	}
	// If resource does not have a room assigned directly, iterate upwards via
	// its owners until we find a room or there is no more owner. The latter
	// may happen (e.g. for Motion Sensors) resulting in room name
	// "<unassigned>".
	currentResourceID := resourceID
	for {
		// Try next owner
		currentResourceID = b.resourceTree[currentResourceID]
		if currentResourceID == "" {
			// No owner left but no room found
			break
		}
		roomName = b.roomAssignments[currentResourceID]
		if roomName != "" {
			// Room name found, done
			return roomName
		}
	}
	return "<unassigned>"
}

func (b *bridge) resolveDeviceName(resourceID string) string {
	deviceName := b.deviceNames[resourceID]
	if deviceName != "" {
		return deviceName
	}
	// If resource does not have a device name assigned directly, iterate
	// upwards via its owners until we find a room or there is no more
	// owner. The latter may happen resulting in device name "<undefined>".
	currentResourceID := resourceID
	for {
		// Try next owner
		currentResourceID = b.resourceTree[currentResourceID]
		if currentResourceID == "" {
			// No owner left but no device found
			break
		}
		deviceName = b.deviceNames[currentResourceID]
		if deviceName != "" {
			// Device name found, done
			return deviceName
		}
	}
	return "<undefined>"
}
