package platform

import (
	"context"
	"database/sql"
	"fmt"
	"math"
	"strings"
	"time"
)

const alertEvaluationVehicleLimit = 50000
const alertCandidateBatchSize = 500
const alertCandidateContinuityWindow = time.Minute
const alertDynamicEvidenceMaxAge = 5 * time.Minute

type alertEvaluationEvidence struct {
	VIN, Plate, Protocol, OEM, Model, Company, SourceEventID, EventAt, ReceivedAt, Location string
	SpeedKmh, SOCPercent, Longitude, Latitude                                               float64
	AlarmFlag                                                                               int64
	FreshnessSec, DataDelaySec                                                              int
}

type alertCandidateState struct {
	FirstMatchedAt, LastMatchedAt time.Time
	SourceEventID                 string
}
type alertActiveEvent struct{ ID, Status string }
type alertCandidateUpsert struct {
	RuleID, VIN, Protocol, SourceEventID string
	FirstMatchedAt, LastMatchedAt        time.Time
	LatestValue                          float64
}
type alertRuleState struct {
	LastValue      float64
	LastObservedAt time.Time
}
type alertRuleStateUpsert struct {
	RuleID, VIN, Protocol string
	LastValue             float64
	ObservedAt            time.Time
}

type alertObservationDecision uint8

const (
	alertObservationAdvanced alertObservationDecision = iota
	alertObservationDuplicate
	alertObservationLate
)

const alertEventInsertSQL = `INSERT INTO vehicle_alert_event(id,fingerprint,rule_id,rule_name,rule_version,severity,status,vin,plate,protocol,metric,operator,trigger_value,threshold_value,threshold_high,unit,duration_sec,location_text,longitude,latitude,source_event_id,event_at,received_at,triggered_at) VALUES(?,?,?,?,?,?,'unprocessed',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP(3))`

func buildAlertEventInsert(id, fingerprint string, rule AlertRule, item alertEvaluationEvidence, value float64) (string, []any) {
	return alertEventInsertSQL, []any{
		id, fingerprint, rule.ID, rule.Name, rule.Version, rule.Severity,
		item.VIN, item.Plate, item.Protocol, rule.Metric, rule.Operator, value,
		rule.Threshold, rule.ThresholdHigh, alertMetricUnit(rule.Metric), rule.DurationSec,
		item.Location, item.Longitude, item.Latitude, item.SourceEventID,
		nullableAlertTime(item.EventAt), nullableAlertTime(item.ReceivedAt),
	}
}

func (s *ProductionStore) EvaluateAlerts(ctx context.Context) (AlertEvaluationResult, error) {
	if err := s.ensureAlertSchema(ctx); err != nil {
		return AlertEvaluationResult{}, err
	}
	rules, err := s.AlertRules(ctx)
	if err != nil {
		return AlertEvaluationResult{}, err
	}
	rules = snapshotAlertRules(rules, s.alertStreamMode)
	if len(rules) == 0 {
		return AlertEvaluationResult{AsOf: time.Now().Format(time.RFC3339)}, nil
	}
	evidence, err := s.alertEvaluationEvidence(ctx)
	if err != nil {
		return AlertEvaluationResult{}, err
	}
	result := AlertEvaluationResult{VehiclesScanned: len(evidence), AsOf: time.Now().Format(time.RFC3339)}
	tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return result, err
	}
	defer tx.Rollback()
	candidates, err := loadAlertCandidates(ctx, tx)
	if err != nil {
		return result, err
	}
	activeEvents, err := loadActiveAlertEvents(ctx, tx)
	if err != nil {
		return result, err
	}
	ruleStates, err := loadAlertRuleStates(ctx, tx)
	if err != nil {
		return result, err
	}
	lastTriggered, err := loadAlertLastTriggered(ctx, tx)
	if err != nil {
		return result, err
	}
	now := time.Now()
	for _, rule := range rules {
		if !rule.Enabled {
			continue
		}
		enabled, lockErr := lockAlertRuleForEvaluation(ctx, tx, rule.ID)
		if lockErr != nil {
			return result, lockErr
		}
		if !enabled {
			continue
		}
		result.RulesEvaluated++
		candidateUpserts := make([]alertCandidateUpsert, 0, len(evidence))
		stateUpserts := make([]alertRuleStateUpsert, 0, len(evidence))
		for _, item := range evidence {
			if !alertRuleInScope(rule, item) {
				continue
			}
			value, supported := alertMetricValue(rule.Metric, item)
			if !supported {
				continue
			}
			fingerprint := rule.ID + "|" + item.VIN + "|" + item.Protocol
			if rule.Metric != "freshness_sec" && time.Duration(item.FreshnessSec)*time.Second > alertDynamicEvidenceMaxAge {
				result.StaleEvidenceSkipped++
				if _, exists := candidates[fingerprint]; exists {
					if _, err = tx.ExecContext(ctx, `DELETE FROM vehicle_alert_candidate WHERE rule_id=? AND vin=? AND protocol=?`, rule.ID, item.VIN, item.Protocol); err != nil {
						return result, err
					}
					delete(candidates, fingerprint)
				}
				continue
			}
			observedAt, clockDriven, observationOK := alertObservationTime(rule.Metric, item, now)
			if !observationOK {
				result.LateObservations++
				continue
			}
			matched := false
			if strings.EqualFold(rule.Operator, "changed") {
				normalized := 0.0
				if value != 0 {
					normalized = 1
				}
				previous, exists := ruleStates[fingerprint]
				if exists && !observedAt.After(previous.LastObservedAt) {
					result.LateObservations++
					continue
				}
				matched = exists && previous.LastValue != normalized
				stateUpserts = append(stateUpserts, alertRuleStateUpsert{RuleID: rule.ID, VIN: item.VIN, Protocol: item.Protocol, LastValue: normalized, ObservedAt: observedAt})
				ruleStates[fingerprint] = alertRuleState{LastValue: normalized, LastObservedAt: observedAt}
			} else {
				matched = compareAlertRuleValue(value, rule)
			}
			if matched {
				if len(activeEvents[fingerprint]) > 0 {
					continue
				}
				candidate, exists := candidates[fingerprint]
				candidate, decision := advanceAlertCandidate(candidate, exists, observedAt, item.SourceEventID, clockDriven)
				switch decision {
				case alertObservationDuplicate:
					result.DuplicateObservations++
					continue
				case alertObservationLate:
					result.LateObservations++
					continue
				}
				candidates[fingerprint] = candidate
				result.CandidatesAdvanced++
				duration := int(candidate.LastMatchedAt.Sub(candidate.FirstMatchedAt).Seconds())
				if duration < rule.DurationSec {
					candidateUpserts = append(candidateUpserts, alertCandidateUpsert{RuleID: rule.ID, VIN: item.VIN, Protocol: item.Protocol, SourceEventID: candidate.SourceEventID, FirstMatchedAt: candidate.FirstMatchedAt, LastMatchedAt: candidate.LastMatchedAt, LatestValue: value})
					continue
				}
				if !alertRepeatAllowed(now, lastTriggered[fingerprint], rule.RepeatIntervalSec) {
					candidateUpserts = append(candidateUpserts, alertCandidateUpsert{RuleID: rule.ID, VIN: item.VIN, Protocol: item.Protocol, SourceEventID: candidate.SourceEventID, FirstMatchedAt: candidate.FirstMatchedAt, LastMatchedAt: candidate.LastMatchedAt, LatestValue: value})
					continue
				}
				id, e := newAlertID("alert")
				if e != nil {
					return result, e
				}
				unit := alertMetricUnit(rule.Metric)
				insertSQL, insertArgs := buildAlertEventInsert(id, fingerprint, rule, item, value)
				_, err = tx.ExecContext(ctx, insertSQL, insertArgs...)
				if err != nil {
					return result, err
				}
				if _, err = tx.ExecContext(ctx, `INSERT INTO vehicle_alert_event_action(event_id,action,from_status,to_status,actor,note) VALUES(?,'trigger','','unprocessed','alert-evaluator','规则命中并达到持续时间')`, id); err != nil {
					return result, err
				}
				for _, channel := range rule.NotificationChannels {
					delivery := "reserved"
					if channel == "in_app" {
						delivery = "created"
					}
					if _, err = tx.ExecContext(ctx, `INSERT INTO vehicle_alert_notification(event_id,title,content,severity,channel,delivery_status) VALUES(?,?,?,?,?,?)`, id, rule.Name, fmt.Sprintf("%s / %s 触发%s：%.2f %s", item.Plate, item.VIN, rule.Name, value, unit), rule.Severity, channel, delivery); err != nil {
						return result, err
					}
				}
				activeEvents[fingerprint] = []alertActiveEvent{{ID: id, Status: "unprocessed"}}
				lastTriggered[fingerprint] = now
				if exists {
					if _, err = tx.ExecContext(ctx, `DELETE FROM vehicle_alert_candidate WHERE rule_id=? AND vin=? AND protocol=?`, rule.ID, item.VIN, item.Protocol); err != nil {
						return result, err
					}
					delete(candidates, fingerprint)
				}
				result.Opened++
			} else {
				if _, exists := candidates[fingerprint]; exists {
					if _, err = tx.ExecContext(ctx, `DELETE FROM vehicle_alert_candidate WHERE rule_id=? AND vin=? AND protocol=?`, rule.ID, item.VIN, item.Protocol); err != nil {
						return result, err
					}
					delete(candidates, fingerprint)
				}
				recovered := true
				if strings.TrimSpace(rule.RecoveryOperator) != "" {
					recovered = compareAlertValue(value, rule.RecoveryOperator, rule.RecoveryThreshold)
				}
				if !recovered {
					continue
				}
				for _, event := range activeEvents[fingerprint] {
					if _, err = tx.ExecContext(ctx, `UPDATE vehicle_alert_event SET status='recovered',recovered_at=CURRENT_TIMESTAMP(3),version=version+1 WHERE id=? AND status=?`, event.ID, event.Status); err != nil {
						return result, err
					}
					if _, err = tx.ExecContext(ctx, `INSERT INTO vehicle_alert_event_action(event_id,action,from_status,to_status,actor,note) VALUES(?,'recover',?,'recovered','alert-evaluator','恢复条件满足')`, event.ID, event.Status); err != nil {
						return result, err
					}
					result.Recovered++
				}
				delete(activeEvents, fingerprint)
			}
		}
		for start := 0; start < len(candidateUpserts); start += alertCandidateBatchSize {
			end := min(start+alertCandidateBatchSize, len(candidateUpserts))
			query, args := buildAlertCandidateUpsert(candidateUpserts[start:end])
			if _, err = tx.ExecContext(ctx, query, args...); err != nil {
				return result, err
			}
		}
		for start := 0; start < len(stateUpserts); start += alertCandidateBatchSize {
			end := min(start+alertCandidateBatchSize, len(stateUpserts))
			query, args := buildAlertRuleStateUpsert(stateUpserts[start:end])
			if _, err = tx.ExecContext(ctx, query, args...); err != nil {
				return result, err
			}
		}
	}
	if err = tx.Commit(); err != nil {
		return result, err
	}
	return result, nil
}

func snapshotAlertRules(rules []AlertRule, streamMode string) []AlertRule {
	if !strings.EqualFold(strings.TrimSpace(streamMode), "active") {
		return rules
	}
	filtered := make([]AlertRule, 0, len(rules))
	for _, rule := range rules {
		if strings.EqualFold(strings.TrimSpace(rule.Metric), "freshness_sec") {
			filtered = append(filtered, rule)
		}
	}
	return filtered
}

func lockAlertRuleForEvaluation(ctx context.Context, tx *sql.Tx, ruleID string) (bool, error) {
	var enabled bool
	err := tx.QueryRowContext(ctx, `SELECT enabled FROM vehicle_alert_rule WHERE id=? FOR UPDATE`, ruleID).Scan(&enabled)
	if err == sql.ErrNoRows {
		return false, nil
	}
	return enabled, err
}

func loadAlertCandidates(ctx context.Context, tx *sql.Tx) (map[string]alertCandidateState, error) {
	rows, err := tx.QueryContext(ctx, `SELECT c.rule_id,c.vin,c.protocol,c.first_matched_at,c.last_matched_at,c.source_event_id FROM vehicle_alert_candidate c JOIN vehicle_alert_rule r ON r.id=c.rule_id AND r.enabled=1`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := map[string]alertCandidateState{}
	for rows.Next() {
		var ruleID, vin, protocol string
		var state alertCandidateState
		if err := rows.Scan(&ruleID, &vin, &protocol, &state.FirstMatchedAt, &state.LastMatchedAt, &state.SourceEventID); err != nil {
			return nil, err
		}
		items[ruleID+"|"+vin+"|"+protocol] = state
	}
	return items, rows.Err()
}

func buildAlertCandidateUpsert(items []alertCandidateUpsert) (string, []any) {
	values := make([]string, 0, len(items))
	args := make([]any, 0, len(items)*7)
	for _, item := range items {
		values = append(values, "(?,?,?,?,?,?,?)")
		args = append(args, item.RuleID, item.VIN, item.Protocol, item.FirstMatchedAt, item.LastMatchedAt, item.LatestValue, item.SourceEventID)
	}
	return `INSERT INTO vehicle_alert_candidate(rule_id,vin,protocol,first_matched_at,last_matched_at,latest_value,source_event_id) VALUES ` + strings.Join(values, ",") + ` ON DUPLICATE KEY UPDATE first_matched_at=VALUES(first_matched_at),last_matched_at=VALUES(last_matched_at),latest_value=VALUES(latest_value),source_event_id=VALUES(source_event_id)`, args
}

func loadAlertRuleStates(ctx context.Context, tx *sql.Tx) (map[string]alertRuleState, error) {
	rows, err := tx.QueryContext(ctx, `SELECT s.rule_id,s.vin,s.protocol,s.observed_value,s.last_observed_at FROM vehicle_alert_rule_state s JOIN vehicle_alert_rule r ON r.id=s.rule_id AND r.enabled=1`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := map[string]alertRuleState{}
	for rows.Next() {
		var ruleID, vin, protocol string
		var state alertRuleState
		if err := rows.Scan(&ruleID, &vin, &protocol, &state.LastValue, &state.LastObservedAt); err != nil {
			return nil, err
		}
		items[ruleID+"|"+vin+"|"+protocol] = state
	}
	return items, rows.Err()
}

func loadAlertLastTriggered(ctx context.Context, tx *sql.Tx) (map[string]time.Time, error) {
	rows, err := tx.QueryContext(ctx, `SELECT e.fingerprint,MAX(e.triggered_at) FROM vehicle_alert_event e JOIN vehicle_alert_rule r ON r.id=e.rule_id AND r.enabled=1 GROUP BY e.fingerprint`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := map[string]time.Time{}
	for rows.Next() {
		var fingerprint string
		var triggeredAt time.Time
		if err := rows.Scan(&fingerprint, &triggeredAt); err != nil {
			return nil, err
		}
		items[fingerprint] = triggeredAt
	}
	return items, rows.Err()
}

func alertRepeatAllowed(now, last time.Time, intervalSec int) bool {
	return intervalSec <= 0 || last.IsZero() || now.Sub(last) >= time.Duration(intervalSec)*time.Second
}

func alertObservationTime(metric string, item alertEvaluationEvidence, now time.Time) (time.Time, bool, bool) {
	if strings.EqualFold(strings.TrimSpace(metric), "freshness_sec") {
		return now, true, true
	}
	for _, value := range []string{item.EventAt, item.ReceivedAt} {
		if parsed, ok := parseAlertEvidenceTime(value); ok {
			return parsed, false, true
		}
	}
	return time.Time{}, false, false
}

func parseAlertEvidenceTime(value string) (time.Time, bool) {
	value = strings.TrimSpace(value)
	if value == "" {
		return time.Time{}, false
	}
	for _, layout := range []string{
		time.RFC3339Nano,
		"2006-01-02 15:04:05.999999",
		"2006-01-02 15:04:05",
	} {
		parsed, err := time.ParseInLocation(layout, value, time.Local)
		if err == nil {
			return parsed, true
		}
	}
	return time.Time{}, false
}

// advanceAlertCandidate makes event-time, not evaluator wall time, the proof of
// duration. Re-reading the same snapshot cannot advance a telemetry candidate.
// freshness_sec is deliberately clock-driven because staleness itself changes
// while the latest source event remains the same.
func advanceAlertCandidate(current alertCandidateState, exists bool, observedAt time.Time, sourceEventID string, clockDriven bool) (alertCandidateState, alertObservationDecision) {
	sourceEventID = strings.TrimSpace(sourceEventID)
	if !exists {
		return alertCandidateState{FirstMatchedAt: observedAt, LastMatchedAt: observedAt, SourceEventID: sourceEventID}, alertObservationAdvanced
	}
	if !clockDriven && sourceEventID != "" && sourceEventID == current.SourceEventID {
		return current, alertObservationDuplicate
	}
	if observedAt.Before(current.LastMatchedAt) {
		return current, alertObservationLate
	}
	if observedAt.Equal(current.LastMatchedAt) {
		if clockDriven || sourceEventID == "" || sourceEventID == current.SourceEventID {
			return current, alertObservationDuplicate
		}
		// A distinct event can share millisecond precision. Remember its ID so
		// subsequent evaluator passes are idempotent, without inventing duration.
		current.SourceEventID = sourceEventID
		return current, alertObservationAdvanced
	}
	if observedAt.Sub(current.LastMatchedAt) > alertCandidateContinuityWindow {
		return alertCandidateState{FirstMatchedAt: observedAt, LastMatchedAt: observedAt, SourceEventID: sourceEventID}, alertObservationAdvanced
	}
	current.LastMatchedAt = observedAt
	current.SourceEventID = sourceEventID
	return current, alertObservationAdvanced
}

func buildAlertRuleStateUpsert(items []alertRuleStateUpsert) (string, []any) {
	values := make([]string, 0, len(items))
	args := make([]any, 0, len(items)*5)
	for _, item := range items {
		values = append(values, "(?,?,?,?,?)")
		args = append(args, item.RuleID, item.VIN, item.Protocol, item.LastValue, item.ObservedAt)
	}
	return `INSERT INTO vehicle_alert_rule_state(rule_id,vin,protocol,observed_value,last_observed_at) VALUES ` + strings.Join(values, ",") + ` ON DUPLICATE KEY UPDATE observed_value=VALUES(observed_value),last_observed_at=VALUES(last_observed_at)`, args
}

func loadActiveAlertEvents(ctx context.Context, tx *sql.Tx) (map[string][]alertActiveEvent, error) {
	rows, err := tx.QueryContext(ctx, `SELECT fingerprint,id,status FROM vehicle_alert_event WHERE status IN ('unprocessed','processing') FOR UPDATE`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := map[string][]alertActiveEvent{}
	for rows.Next() {
		var fingerprint string
		var event alertActiveEvent
		if err := rows.Scan(&fingerprint, &event.ID, &event.Status); err != nil {
			return nil, err
		}
		items[fingerprint] = append(items[fingerprint], event)
	}
	return items, rows.Err()
}

func (s *ProductionStore) alertEvaluationEvidence(ctx context.Context) ([]alertEvaluationEvidence, error) {
	rows, err := s.db.QueryContext(ctx, `SELECT l.vin,COALESCE(NULLIF(l.plate,''),NULLIF(b.plate,''),''),l.protocol,COALESCE(b.oem,''),COALESCE(p.model_name,''),COALESCE(p.company_name,''),COALESCE(l.speed_kmh,0),COALESCE(l.soc_percent,0),COALESCE(l.alarm_flag,0),COALESCE(l.longitude,0),COALESCE(l.latitude,0),COALESCE(l.event_id,''),COALESCE(DATE_FORMAT(l.event_time,'%Y-%m-%d %H:%i:%s.%f'),''),COALESCE(DATE_FORMAT(l.received_at,'%Y-%m-%d %H:%i:%s.%f'),''),GREATEST(0,TIMESTAMPDIFF(SECOND,l.updated_at,NOW())),TIMESTAMPDIFF(SECOND,l.event_time,l.received_at),COALESCE(CONCAT(l.longitude,',',l.latitude),'') FROM vehicle_realtime_location l LEFT JOIN vehicle_identity_binding b ON b.vin=l.vin LEFT JOIN vehicle_profile p ON p.vin=l.vin WHERE l.vin<>'' ORDER BY l.updated_at DESC LIMIT ?`, alertEvaluationVehicleLimit+1)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := make([]alertEvaluationEvidence, 0, 1024)
	for rows.Next() {
		var item alertEvaluationEvidence
		var delay sql.NullInt64
		if err = rows.Scan(&item.VIN, &item.Plate, &item.Protocol, &item.OEM, &item.Model, &item.Company, &item.SpeedKmh, &item.SOCPercent, &item.AlarmFlag, &item.Longitude, &item.Latitude, &item.SourceEventID, &item.EventAt, &item.ReceivedAt, &item.FreshnessSec, &delay, &item.Location); err != nil {
			return nil, err
		}
		if delay.Valid {
			item.DataDelaySec = int(delay.Int64)
		}
		items = append(items, item)
		if len(items) > alertEvaluationVehicleLimit {
			return nil, fmt.Errorf("alert evaluation evidence exceeds safety limit %d", alertEvaluationVehicleLimit)
		}
	}
	return items, rows.Err()
}

func alertRuleInScope(rule AlertRule, item alertEvaluationEvidence) bool {
	if len(rule.ScopeProtocols) > 0 && !containsFold(rule.ScopeProtocols, item.Protocol) {
		return false
	}
	if len(rule.ScopeVINs) > 0 && !containsFold(rule.ScopeVINs, item.VIN) {
		return false
	}
	if len(rule.ScopeOEMs) > 0 && !containsFold(rule.ScopeOEMs, item.OEM) {
		return false
	}
	if len(rule.ScopeModels) > 0 && !containsFold(rule.ScopeModels, item.Model) {
		return false
	}
	if len(rule.ScopeCompanies) > 0 && !containsFold(rule.ScopeCompanies, item.Company) {
		return false
	}
	return true
}
func containsFold(values []string, target string) bool {
	for _, value := range values {
		if strings.EqualFold(value, target) {
			return true
		}
	}
	return false
}
func alertMetricValue(metric string, item alertEvaluationEvidence) (float64, bool) {
	switch strings.ToLower(strings.TrimSpace(metric)) {
	case "speed_kmh":
		return item.SpeedKmh, true
	case "soc_percent":
		return item.SOCPercent, true
	case "alarm_active":
		if item.AlarmFlag != 0 {
			return 1, true
		}
		return 0, true
	case "freshness_sec":
		return float64(item.FreshnessSec), true
	case "data_delay_sec":
		return float64(item.DataDelaySec), true
	}
	return 0, false
}
func compareAlertValue(value float64, operator string, threshold float64) bool {
	switch strings.ToLower(operator) {
	case "gt":
		return value > threshold
	case "gte":
		return value >= threshold
	case "lt":
		return value < threshold
	case "lte":
		return value <= threshold
	case "eq":
		return math.Abs(value-threshold) < 1e-9
	case "neq":
		return math.Abs(value-threshold) >= 1e-9
	}
	return false
}

func compareAlertRuleValue(value float64, rule AlertRule) bool {
	switch strings.ToLower(rule.Operator) {
	case "between":
		return value >= rule.Threshold && value <= rule.ThresholdHigh
	case "outside":
		return value < rule.Threshold || value > rule.ThresholdHigh
	default:
		return compareAlertValue(value, rule.Operator, rule.Threshold)
	}
}
func alertMetricUnit(metric string) string {
	switch metric {
	case "speed_kmh":
		return "km/h"
	case "soc_percent":
		return "%"
	case "freshness_sec", "data_delay_sec":
		return "秒"
	}
	return ""
}
func nullableAlertTime(value string) any {
	if strings.TrimSpace(value) == "" {
		return nil
	}
	return value
}
