package identity

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

type SnapshotRefreshResult struct {
	BindingEntries      int
	IdentifierEntries   int
	RegistrationEntries int
	SourceEntries       int
	RefreshedAt         time.Time
}

// identitySnapshot is immutable after atomic publication, so frame handling
// performs only local map lookups and never waits for MySQL or a refresh lock.
type identitySnapshot struct {
	bindings      map[string]string
	identifiers   map[string]vehicleIdentifierMatch
	registrations map[string]registrationCacheEntry
	sources       map[string]sourceMetadata
	refreshedAt   time.Time
}

func (r *MySQLResolver) RefreshSnapshot(ctx context.Context) (SnapshotRefreshResult, error) {
	if r == nil || r.db == nil {
		return SnapshotRefreshResult{}, fmt.Errorf("identity snapshot database is not configured")
	}
	r.snapshotRefreshMu.Lock()
	defer r.snapshotRefreshMu.Unlock()

	next := &identitySnapshot{
		bindings:      map[string]string{},
		identifiers:   map[string]vehicleIdentifierMatch{},
		registrations: map[string]registrationCacheEntry{},
		sources:       map[string]sourceMetadata{},
	}
	if err := r.loadBindingSnapshot(ctx, next); err != nil {
		return SnapshotRefreshResult{}, err
	}
	if err := r.loadIdentifierSnapshot(ctx, next); err != nil {
		return SnapshotRefreshResult{}, err
	}
	if err := r.loadRegistrationSnapshot(ctx, next); err != nil {
		return SnapshotRefreshResult{}, err
	}
	if err := r.loadSourceSnapshot(ctx, next); err != nil {
		return SnapshotRefreshResult{}, err
	}
	next.refreshedAt = time.Now()

	r.snapshot.Store(next)
	return SnapshotRefreshResult{
		BindingEntries:      len(next.bindings),
		IdentifierEntries:   len(next.identifiers),
		RegistrationEntries: len(next.registrations),
		SourceEntries:       len(next.sources),
		RefreshedAt:         next.refreshedAt,
	}, nil
}

func (r *MySQLResolver) loadBindingSnapshot(ctx context.Context, target *identitySnapshot) error {
	rows, err := r.db.QueryContext(ctx, "SELECT vin, plate, phone FROM "+r.table+" WHERE vin IS NOT NULL AND TRIM(vin) <> ''")
	if err != nil {
		return fmt.Errorf("load identity binding snapshot: %w", err)
	}
	defer rows.Close()
	ambiguous := map[string]struct{}{}
	for rows.Next() {
		var vin, plate, phone sql.NullString
		if err := rows.Scan(&vin, &plate, &phone); err != nil {
			return fmt.Errorf("scan identity binding snapshot: %w", err)
		}
		vinValue := strings.TrimSpace(vin.String)
		if vinValue == "" {
			continue
		}
		addSnapshotBinding(target.bindings, ambiguous, bindingSnapshotKey("vin", vinValue), vinValue)
		if value := strings.TrimSpace(plate.String); value != "" {
			addSnapshotBinding(target.bindings, ambiguous, bindingSnapshotKey("plate", value), vinValue)
		}
		if value := normalizePhone(phone.String); value != "" {
			addSnapshotBinding(target.bindings, ambiguous, bindingSnapshotKey("phone", value), vinValue)
		}
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("iterate identity binding snapshot: %w", err)
	}
	return nil
}

func (r *MySQLResolver) loadIdentifierSnapshot(ctx context.Context, target *identitySnapshot) error {
	rows, err := r.db.QueryContext(ctx, `SELECT protocol, source_code, identifier_type, identifier_value,
       vin, COALESCE(NULLIF(TRIM(oem), ''), source_code) AS platform_name
FROM vehicle_identifier
WHERE enabled = 1 AND vin IS NOT NULL AND TRIM(vin) <> ''
  AND identifier_value IS NOT NULL AND TRIM(identifier_value) <> ''`)
	if err != nil {
		return fmt.Errorf("load vehicle identifier snapshot: %w", err)
	}
	defer rows.Close()
	ambiguous := map[string]struct{}{}
	for rows.Next() {
		var protocol, sourceCode, identifierType, identifierValue, vin, platformName sql.NullString
		if err := rows.Scan(&protocol, &sourceCode, &identifierType, &identifierValue, &vin, &platformName); err != nil {
			return fmt.Errorf("scan vehicle identifier snapshot: %w", err)
		}
		protocolValue := strings.TrimSpace(protocol.String)
		typeValue := strings.ToUpper(strings.TrimSpace(identifierType.String))
		value := normalizeIdentifierValue(typeValue, identifierValue.String)
		vinValue := strings.TrimSpace(vin.String)
		if protocolValue == "" || typeValue == "" || value == "" || vinValue == "" {
			continue
		}
		match := vehicleIdentifierMatch{
			VIN:          vinValue,
			SourceCode:   strings.TrimSpace(sourceCode.String),
			PlatformName: strings.TrimSpace(platformName.String),
		}
		scopedKey := vehicleIdentifierSnapshotKey(protocolValue, match.SourceCode, typeValue, value)
		addSnapshotIdentifier(target.identifiers, ambiguous, scopedKey, match)
		globalKey := vehicleIdentifierSnapshotKey(protocolValue, "", typeValue, value)
		addSnapshotIdentifier(target.identifiers, ambiguous, globalKey, match)
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("iterate vehicle identifier snapshot: %w", err)
	}
	return nil
}

func (r *MySQLResolver) loadRegistrationSnapshot(ctx context.Context, target *identitySnapshot) error {
	rows, err := r.db.QueryContext(ctx, `SELECT phone, vin, device_id, plate, auth_token
FROM jt808_registration
WHERE phone IS NOT NULL AND TRIM(phone) <> ''`)
	if err != nil {
		return fmt.Errorf("load jt808 registration snapshot: %w", err)
	}
	defer rows.Close()
	for rows.Next() {
		var phone, vin, deviceID, plate, authToken sql.NullString
		if err := rows.Scan(&phone, &vin, &deviceID, &plate, &authToken); err != nil {
			return fmt.Errorf("scan jt808 registration snapshot: %w", err)
		}
		phoneValue := normalizePhone(phone.String)
		if phoneValue == "" {
			continue
		}
		target.registrations[phoneValue] = registrationCacheEntry{
			vin:       strings.TrimSpace(vin.String),
			deviceID:  strings.TrimSpace(deviceID.String),
			plate:     strings.TrimSpace(plate.String),
			authToken: strings.TrimSpace(authToken.String),
		}
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("iterate jt808 registration snapshot: %w", err)
	}
	return nil
}

func (r *MySQLResolver) loadSourceSnapshot(ctx context.Context, target *identitySnapshot) error {
	rows, err := r.db.QueryContext(ctx, `SELECT protocol, source_ip, source_code, platform_name, source_kind
FROM vehicle_data_source
WHERE enabled = 1 AND source_ip IS NOT NULL AND TRIM(source_ip) <> ''
  AND (
    (source_code IS NOT NULL AND TRIM(source_code) <> '')
    OR source_kind IN ('PLATFORM', 'DIRECT')
    OR (platform_name IS NOT NULL AND TRIM(platform_name) <> '')
  )`)
	if err != nil {
		if isOptionalSourceCodeLookupError(err) {
			return nil
		}
		return fmt.Errorf("load vehicle data source snapshot: %w", err)
	}
	defer rows.Close()
	for rows.Next() {
		var protocol, sourceIP, sourceCode, platformName, sourceKind sql.NullString
		if err := rows.Scan(&protocol, &sourceIP, &sourceCode, &platformName, &sourceKind); err != nil {
			return fmt.Errorf("scan vehicle data source snapshot: %w", err)
		}
		key := sourceSnapshotKey(protocol.String, sourceIP.String)
		if key == "" {
			continue
		}
		target.sources[key] = sourceMetadata{
			SourceCode:   strings.TrimSpace(sourceCode.String),
			PlatformName: strings.TrimSpace(platformName.String),
			SourceKind:   strings.TrimSpace(sourceKind.String),
		}
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("iterate vehicle data source snapshot: %w", err)
	}
	return nil
}

func addSnapshotBinding(values map[string]string, ambiguous map[string]struct{}, key string, vin string) {
	if key == "" {
		return
	}
	if _, exists := ambiguous[key]; exists {
		return
	}
	if current, exists := values[key]; exists && !strings.EqualFold(current, vin) {
		delete(values, key)
		ambiguous[key] = struct{}{}
		return
	}
	values[key] = vin
}

func addSnapshotIdentifier(values map[string]vehicleIdentifierMatch, ambiguous map[string]struct{}, key string, match vehicleIdentifierMatch) {
	if key == "" {
		return
	}
	if _, exists := ambiguous[key]; exists {
		return
	}
	current, exists := values[key]
	if !exists {
		values[key] = match
		return
	}
	if !strings.EqualFold(current.VIN, match.VIN) {
		delete(values, key)
		ambiguous[key] = struct{}{}
		return
	}
	if !strings.EqualFold(current.SourceCode, match.SourceCode) {
		current.SourceCode = ""
		current.PlatformName = ""
	} else if current.PlatformName != match.PlatformName {
		current.PlatformName = ""
	}
	values[key] = current
}

func (r *MySQLResolver) snapshotBinding(column string, value string) (string, bool) {
	key := bindingSnapshotKey(column, value)
	if key == "" {
		return "", false
	}
	snapshot := r.snapshot.Load()
	if snapshot == nil {
		return "", false
	}
	vin, ok := snapshot.bindings[key]
	return vin, ok
}

func (r *MySQLResolver) snapshotIdentifier(protocolValue string, sourceCode string, identifierType string, value string) (vehicleIdentifierMatch, bool) {
	key := vehicleIdentifierSnapshotKey(protocolValue, sourceCode, identifierType, value)
	snapshot := r.snapshot.Load()
	if snapshot == nil {
		return vehicleIdentifierMatch{}, false
	}
	match, ok := snapshot.identifiers[key]
	return match, ok
}

func (r *MySQLResolver) snapshotRegistration(phone string) (registrationCacheEntry, bool) {
	phone = normalizePhone(phone)
	snapshot := r.snapshot.Load()
	if snapshot == nil {
		return registrationCacheEntry{}, false
	}
	entry, ok := snapshot.registrations[phone]
	return entry, ok
}

func (r *MySQLResolver) snapshotSource(protocolValue string, sourceIP string) (sourceMetadata, bool) {
	key := sourceSnapshotKey(protocolValue, sourceIP)
	snapshot := r.snapshot.Load()
	if snapshot == nil {
		return sourceMetadata{}, false
	}
	metadata, ok := snapshot.sources[key]
	return metadata, ok
}

// JT808AuthToken serves authentication from the same immutable snapshot used
// by identity resolution. It deliberately never falls back to a per-frame SQL
// query because authentication is on the protocol response hot path.
func (r *MySQLResolver) JT808AuthToken(phone string) (string, bool) {
	entry, ok := r.snapshotRegistration(phone)
	token := strings.TrimSpace(entry.authToken)
	return token, ok && token != ""
}

func bindingSnapshotKey(column string, value string) string {
	column = strings.ToLower(strings.TrimSpace(column))
	value = strings.TrimSpace(value)
	if column == "phone" {
		value = normalizePhone(value)
	}
	if value == "" || (column != "vin" && column != "plate" && column != "phone") {
		return ""
	}
	return column + "\x00" + value
}

func vehicleIdentifierSnapshotKey(protocolValue string, sourceCode string, identifierType string, value string) string {
	protocolValue = strings.TrimSpace(protocolValue)
	sourceCode = strings.TrimSpace(sourceCode)
	identifierType = strings.ToUpper(strings.TrimSpace(identifierType))
	value = normalizeIdentifierValue(identifierType, value)
	if protocolValue == "" || identifierType == "" || value == "" {
		return ""
	}
	return "vehicle_identifier\x00" + protocolValue + "\x00" + sourceCode + "\x00" + identifierType + "\x00" + value
}

func sourceSnapshotKey(protocolValue string, sourceIP string) string {
	protocolValue = strings.TrimSpace(protocolValue)
	sourceIP = normalizeEndpointIP(sourceIP)
	if protocolValue == "" || sourceIP == "" {
		return ""
	}
	return protocolValue + "\x00" + sourceIP
}
