/*
SPDX-FileCopyrightText: Red Hat

SPDX-License-Identifier: Apache-2.0
*/

package alertmanager

import (
	"context"
	"log/slog"
	"time"

	"github.com/google/uuid"
	api "github.com/openshift-kni/oran-o2ims/internal/service/alarms/api/generated"
	"github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/db/models"
	"github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure"
	"github.com/openshift-kni/oran-o2ims/internal/service/common"
)

// ConvertAmToAlarmEventRecordModels get alarmEventRecords based on the alertmanager notification and AlarmDefinition
func ConvertAmToAlarmEventRecordModels(ctx context.Context, alerts *[]api.Alert, clusterServer, resourceServer infrastructure.Client) []models.AlarmEventRecord {
	records := make([]models.AlarmEventRecord, 0, len(*alerts))
	for _, alert := range *alerts {
		record := models.AlarmEventRecord{}

		// Validate startsAt is always there
		if alert.StartsAt != nil && !alert.StartsAt.IsZero() {
			record.AlarmRaisedTime = *alert.StartsAt
		} else {
			slog.ErrorContext(ctx, "Alert StartsAt is required, skipping.", slog.Any("alert", alert))
			continue
		}

		// Validate status is always there, also derive the PerceivedSeverity as needed
		if alert.Status != nil {
			record.AlarmStatus = string(*alert.Status)

			// Get labels safely
			labels := getLabels(alert)

			// Make sure the current payload has the right severity
			if *alert.Status == api.Resolved {
				record.PerceivedSeverity = severityToPerceivedSeverity("cleared")
			} else {
				ps, _ := getPerceivedSeverity(labels)
				record.PerceivedSeverity = ps
			}
		} else {
			slog.ErrorContext(ctx, "Alert Status is required, skipping.", slog.Any("alert", alert))
			continue
		}

		// Validate Fingerprint is always there
		if alert.Fingerprint != nil {
			record.Fingerprint = *alert.Fingerprint
		} else {
			slog.ErrorContext(ctx, "Alert Fingerprint is required, skipping.", slog.Any("alert", alert))
			continue
		}

		if alert.EndsAt != nil && !alert.EndsAt.IsZero() {
			record.AlarmClearedTime = alert.EndsAt
		}

		// Get labels
		labels := getLabels(alert)

		// Update Extensions with things we didn't really process
		record.Extensions = getExtensions(alert)

		// Determine alert type (CaaS vs hardware) and select appropriate infrastructure client
		// Hardware alerts have both type=hardware AND component=ironic labels
		isHardware := isHardwareAlert(labels)
		var infrastructureClient infrastructure.Client
		if isHardware {
			// For hardware alerts, object is the resource ID from instance_uuid
			record.ObjectID = getResourceID(labels)
			record.AlarmSource = models.AlarmSourceHardware
			infrastructureClient = resourceServer
		} else {
			// For CaaS alerts, object is the cluster ID from managed_cluster
			record.ObjectID = getClusterID(labels)
			record.AlarmSource = models.AlarmSourceCaaS
			infrastructureClient = clusterServer
		}

		// derive ObjectTypeID from ObjectID
		if record.ObjectID != nil {
			objectTypeID, err := infrastructureClient.GetObjectTypeID(ctx, *record.ObjectID)
			if err != nil {
				slog.WarnContext(ctx, "Could not get object type ID", slog.String("objectID", record.ObjectID.String()), slog.Any("error", err))
			} else {
				record.ObjectTypeID = &objectTypeID
			}
		}

		// See if possible to pick up additional info from its definition
		if record.ObjectTypeID != nil {
			// Severity from PromRule (shows up in alert's label)
			_, severity := getPerceivedSeverity(labels)
			alarmDefinitionID, err := infrastructureClient.GetAlarmDefinitionID(ctx, *record.ObjectTypeID, getAlertName(labels), severity)
			if err != nil {
				slog.WarnContext(ctx, "Could not get alarm definition ID", slog.String("objectTypeID", record.ObjectTypeID.String()), slog.String("name", getAlertName(labels)), slog.String("severity", severity), slog.Any("error", err))
			} else {
				record.AlarmDefinitionID = &alarmDefinitionID
			}
		}

		// Anything else that's not mentioned explicitly will be handled by DB such ID generation and default values as needed.
		records = append(records, record)
	}

	slog.InfoContext(ctx, "Converted alerts", slog.Int("records", len(records)))
	return records
}

func getClusterID(labels map[string]string) *uuid.UUID {
	// First try managed_cluster (standard managed cluster alerts)
	val, ok := labels["managed_cluster"]
	if !ok {
		// Fall back to clusterID (Thanos/ACM alerts like ViolatedPolicyReport)
		val, ok = labels["clusterID"]
		if !ok {
			slog.Warn("Could not find managed_cluster or clusterID", slog.Any("labels", labels))
			return nil
		}
	}

	id, err := uuid.Parse(val)
	if err != nil {
		slog.Warn("Could not convert cluster ID string to uuid", slog.Any("labels", labels), slog.Any("error", err))
		return nil
	}

	return &id
}

// isHardwareAlert checks if an alert is a hardware alert by checking for type=hardware AND component=ironic labels
func isHardwareAlert(labels map[string]string) bool {
	alertType, hasType := labels[common.HardwareAlertTypeLabel]
	component, hasComponent := labels[common.HardwareAlertComponentLabel]
	return hasType && alertType == common.HardwareAlertTypeValue && hasComponent && component == common.HardwareAlertComponentValue
}

// getResourceID extracts resource ID from instance_uuid label for hardware alerts
func getResourceID(labels map[string]string) *uuid.UUID {
	val, ok := labels["instance_uuid"]
	if !ok {
		slog.Warn("Could not find instance_uuid for hardware alert", slog.Any("labels", labels))
		return nil
	}

	id, err := uuid.Parse(val)
	if err != nil {
		slog.Warn("Could not convert instance_uuid string to uuid", slog.Any("labels", labels), slog.Any("error", err))
		return nil
	}

	return &id
}

// getAlertName extract name from alert label
func getAlertName(labels map[string]string) string {
	val, ok := labels["alertname"]
	if !ok {
		// this may never execute but keeping a check just in case
		slog.Warn("Could not find alertname", slog.Any("labels", labels))
		return "Unknown"
	}

	return val
}

// getPerceivedSeverity am's `severity` to oran's PerceivedSeverity
func getPerceivedSeverity(labels map[string]string) (api.PerceivedSeverity, string) {
	severity, ok := labels["severity"]
	if !ok {
		slog.Warn("Could not find severity label", slog.Any("labels", labels))
		return api.INDETERMINATE, ""
	}

	return severityToPerceivedSeverity(severity), severity
}

func severityToPerceivedSeverity(input string) api.PerceivedSeverity {
	switch input {
	case "cleared":
		return api.CLEARED
	case "critical":
		return api.CRITICAL
	case "major", "important": // "important" is used by ACM/PolicyReport (ViolatedPolicyReport)
		return api.MAJOR
	case "minor", "low":
		return api.MINOR
	case "warning", "info", "moderate": // "moderate" is used by ACM/PolicyReport (ViolatedPolicyReport)
		return api.WARNING
	default:
		slog.Debug("Unknown severity mapped to INDETERMINATE", slog.String("severity", input))
		return api.INDETERMINATE
	}
}

// getExtensions extract oran extension from alert
func getExtensions(a api.Alert) map[string]interface{} {
	result := make(map[string]interface{})

	for k, v := range getLabels(a) {
		result[k] = v
	}
	for k, v := range getAnnotations(a) {
		result[k] = v
	}

	if a.GeneratorURL != nil {
		result["generatorURL"] = *a.GeneratorURL
	}

	return result
}

// Safely get labels
func getLabels(alert api.Alert) map[string]string {
	if alert.Labels == nil {
		return make(map[string]string)
	}
	return *alert.Labels
}

// Safely get annotations
func getAnnotations(alert api.Alert) map[string]string {
	if alert.Annotations == nil {
		return make(map[string]string)
	}
	return *alert.Annotations
}

// ConvertAPIAlertsToWebhook converts a slice of API into a slice of Webhook.
func ConvertAPIAlertsToWebhook(apiAlerts *[]APIAlert) []api.Alert {
	// Handle nil input
	if apiAlerts == nil {
		return []api.Alert{}
	}

	// Handle empty input array
	if len(*apiAlerts) == 0 {
		return []api.Alert{}
	}

	webhookAlerts := make([]api.Alert, 0, len(*apiAlerts))
	now := time.Now().UTC()

	for _, a := range *apiAlerts {
		// Determine the alert status based on endsAt compared to current time.
		//
		// This is strange but API call will always have an `endsAt` regardless if an alert actually "ended"/resolved.
		// The value of `endsAt` is either future or time in the past.
		// For alerts coming from Prometheus it will always have an "endsAt" from source but in other cases AM will add do 'endsAt = now() + resolved_time'...but this is not applicable for us.
		// More info: https://prometheus.io/docs/alerting/latest/configuration/#file-layout-and-global-settings
		// A time in past indicates an alert is resolved.
		//
		// A resolved alerts (i.e. endsAt from past) is set to be removed from the '/alerts' but depending on when we call the API
		// we may still see "resolved" alerts, which is why this need to be handled correctly.
		var (
			status      api.AlertmanagerNotificationStatus
			finalEndsAt *time.Time
		)

		// Assume alert is firing as this is likely case
		status = api.Firing
		finalEndsAt = nil

		// If endsAt in the past it's resolved
		if a.EndsAt != nil && now.After(*a.EndsAt) {
			status = api.Resolved
			finalEndsAt = a.EndsAt
		}

		// Other than endsAt and status (which needs to be computed) everything else is a pass-through
		// to make API payload to a webhook payload
		webhookAlert := api.Alert{
			Annotations:  a.Annotations,
			Labels:       a.Labels,
			StartsAt:     a.StartsAt,
			EndsAt:       finalEndsAt,
			Fingerprint:  a.Fingerprint,
			GeneratorURL: a.GeneratorURL,
			Status:       &status,
		}

		webhookAlerts = append(webhookAlerts, webhookAlert)
	}

	slog.Info("Converted from API to Webhook", slog.Int("alerts", len(webhookAlerts)))
	return webhookAlerts
}
