// Package memory provides operational memory for AI context.
// It tracks changes to infrastructure, remediation actions, and their outcomes
// to enable the AI to provide more informed, experience-based recommendations.
package memory

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"sync"
	"time"

	"github.com/rs/zerolog/log"
)

// ChangeType represents the type of infrastructure change detected
type ChangeType string

const (
	ChangeCreated   ChangeType = "created"   // New resource appeared
	ChangeDeleted   ChangeType = "deleted"   // Resource removed
	ChangeConfig    ChangeType = "config"    // Configuration changed (RAM, CPU, etc)
	ChangeStatus    ChangeType = "status"    // Status changed (started, stopped, paused)
	ChangeMigrated  ChangeType = "migrated"  // Moved to different node
	ChangeRestarted ChangeType = "restarted" // Resource was restarted
	ChangeBackedUp  ChangeType = "backed_up" // Backup completed
)

// Change represents a detected change to infrastructure
type Change struct {
	ID           string      `json:"id"`
	ResourceID   string      `json:"resource_id"`
	ResourceType string      `json:"resource_type"` // vm, container, node, storage
	ResourceName string      `json:"resource_name"`
	ChangeType   ChangeType  `json:"change_type"`
	Before       interface{} `json:"before,omitempty"`
	After        interface{} `json:"after,omitempty"`
	DetectedAt   time.Time   `json:"detected_at"`
	Description  string      `json:"description"`
}

// ResourceSnapshot captures key attributes for change detection
type ResourceSnapshot struct {
	ID           string    `json:"id"`
	Name         string    `json:"name"`
	Type         string    `json:"type"`
	Status       string    `json:"status"`
	Node         string    `json:"node,omitempty"`
	CPUCores     int       `json:"cpu_cores,omitempty"`
	MemoryBytes  int64     `json:"memory_bytes,omitempty"`
	DiskBytes    int64     `json:"disk_bytes,omitempty"`
	LastBackup   time.Time `json:"last_backup,omitempty"`
	SnapshotTime time.Time `json:"snapshot_time"`
}

// ChangeDetector tracks infrastructure changes over time
type ChangeDetector struct {
	mu            sync.RWMutex
	previousState map[string]ResourceSnapshot // resourceID -> snapshot
	changes       []Change
	maxChanges    int

	// Persistence
	dataDir string
}

// ChangeDetectorConfig configures the change detector
type ChangeDetectorConfig struct {
	MaxChanges int    // Maximum changes to retain (default: 1000)
	DataDir    string // Directory for persistence
}

// NewChangeDetector creates a new change detector
func NewChangeDetector(cfg ChangeDetectorConfig) *ChangeDetector {
	if cfg.MaxChanges <= 0 {
		cfg.MaxChanges = 1000
	}

	d := &ChangeDetector{
		previousState: make(map[string]ResourceSnapshot),
		changes:       make([]Change, 0),
		maxChanges:    cfg.MaxChanges,
		dataDir:       cfg.DataDir,
	}

	// Load existing changes from disk
	if cfg.DataDir != "" {
		if err := d.loadFromDisk(); err != nil {
			log.Warn().Err(err).Msg("Failed to load change history from disk")
		} else if len(d.changes) > 0 {
			log.Info().Int("count", len(d.changes)).Msg("Loaded change history from disk")
		}
	}

	return d
}

// DetectChanges compares current snapshots to previous state and detects changes
func (d *ChangeDetector) DetectChanges(currentSnapshots []ResourceSnapshot) []Change {
	d.mu.Lock()
	defer d.mu.Unlock()

	now := time.Now()
	var newChanges []Change

	// Track which resources we've seen in current snapshot
	currentIDs := make(map[string]bool)

	for _, current := range currentSnapshots {
		currentIDs[current.ID] = true

		prev, exists := d.previousState[current.ID]
		if !exists {
			// New resource
			change := Change{
				ID:           generateChangeID(),
				ResourceID:   current.ID,
				ResourceType: current.Type,
				ResourceName: current.Name,
				ChangeType:   ChangeCreated,
				After:        current,
				DetectedAt:   now,
				Description:  formatCreateDescription(current),
			}
			newChanges = append(newChanges, change)
		} else {
			// Check for changes
			changes := d.detectResourceChanges(prev, current, now)
			newChanges = append(newChanges, changes...)
		}

		// Update previous state
		d.previousState[current.ID] = current
	}

	// Check for deleted resources
	for id, prev := range d.previousState {
		if !currentIDs[id] {
			change := Change{
				ID:           generateChangeID(),
				ResourceID:   id,
				ResourceType: prev.Type,
				ResourceName: prev.Name,
				ChangeType:   ChangeDeleted,
				Before:       prev,
				DetectedAt:   now,
				Description:  formatDeleteDescription(prev),
			}
			newChanges = append(newChanges, change)
			delete(d.previousState, id)
		}
	}

	// Store new changes
	if len(newChanges) > 0 {
		d.changes = append(d.changes, newChanges...)
		d.trimChanges()

		// Persist asynchronously
		go func() {
			if err := d.saveToDisk(); err != nil {
				log.Warn().Err(err).Msg("Failed to save change history")
			}
		}()
	}

	return newChanges
}

// detectResourceChanges checks for changes between two snapshots of the same resource
func (d *ChangeDetector) detectResourceChanges(prev, current ResourceSnapshot, now time.Time) []Change {
	var changes []Change

	// Status change
	if prev.Status != current.Status {
		change := Change{
			ID:           generateChangeID(),
			ResourceID:   current.ID,
			ResourceType: current.Type,
			ResourceName: current.Name,
			ChangeType:   ChangeStatus,
			Before:       prev.Status,
			After:        current.Status,
			DetectedAt:   now,
			Description:  formatStatusDescription(current.Name, prev.Status, current.Status),
		}
		changes = append(changes, change)
	}

	// Node change (migration)
	if prev.Node != "" && current.Node != "" && prev.Node != current.Node {
		change := Change{
			ID:           generateChangeID(),
			ResourceID:   current.ID,
			ResourceType: current.Type,
			ResourceName: current.Name,
			ChangeType:   ChangeMigrated,
			Before:       prev.Node,
			After:        current.Node,
			DetectedAt:   now,
			Description:  formatMigrationDescription(current.Name, prev.Node, current.Node),
		}
		changes = append(changes, change)
	}

	// CPU change
	if prev.CPUCores > 0 && current.CPUCores > 0 && prev.CPUCores != current.CPUCores {
		change := Change{
			ID:           generateChangeID(),
			ResourceID:   current.ID,
			ResourceType: current.Type,
			ResourceName: current.Name,
			ChangeType:   ChangeConfig,
			Before:       map[string]int{"cpu_cores": prev.CPUCores},
			After:        map[string]int{"cpu_cores": current.CPUCores},
			DetectedAt:   now,
			Description:  formatCPUChangeDescription(current.Name, prev.CPUCores, current.CPUCores),
		}
		changes = append(changes, change)
	}

	// Memory change (significant change > 5%)
	if prev.MemoryBytes > 0 && current.MemoryBytes > 0 {
		pctChange := float64(current.MemoryBytes-prev.MemoryBytes) / float64(prev.MemoryBytes)
		if pctChange > 0.05 || pctChange < -0.05 {
			change := Change{
				ID:           generateChangeID(),
				ResourceID:   current.ID,
				ResourceType: current.Type,
				ResourceName: current.Name,
				ChangeType:   ChangeConfig,
				Before:       map[string]int64{"memory_bytes": prev.MemoryBytes},
				After:        map[string]int64{"memory_bytes": current.MemoryBytes},
				DetectedAt:   now,
				Description:  formatMemoryChangeDescription(current.Name, prev.MemoryBytes, current.MemoryBytes),
			}
			changes = append(changes, change)
		}
	}

	// Backup completed
	if !prev.LastBackup.IsZero() && !current.LastBackup.IsZero() &&
		current.LastBackup.After(prev.LastBackup) {
		change := Change{
			ID:           generateChangeID(),
			ResourceID:   current.ID,
			ResourceType: current.Type,
			ResourceName: current.Name,
			ChangeType:   ChangeBackedUp,
			Before:       prev.LastBackup,
			After:        current.LastBackup,
			DetectedAt:   now,
			Description:  formatBackupDescription(current.Name, current.LastBackup),
		}
		changes = append(changes, change)
	}

	return changes
}

// GetChangesForResource returns recent changes for a specific resource
func (d *ChangeDetector) GetChangesForResource(resourceID string, limit int) []Change {
	d.mu.RLock()
	defer d.mu.RUnlock()

	var result []Change
	// Iterate in reverse to get most recent first
	for i := len(d.changes) - 1; i >= 0 && len(result) < limit; i-- {
		if d.changes[i].ResourceID == resourceID {
			result = append(result, d.changes[i])
		}
	}
	return result
}

// GetRecentChanges returns the most recent changes across all resources
func (d *ChangeDetector) GetRecentChanges(limit int, since time.Time) []Change {
	d.mu.RLock()
	defer d.mu.RUnlock()

	var result []Change
	for i := len(d.changes) - 1; i >= 0 && len(result) < limit; i-- {
		if d.changes[i].DetectedAt.After(since) {
			result = append(result, d.changes[i])
		}
	}
	return result
}

// GetChangesSummary returns a formatted summary of recent changes for AI context
func (d *ChangeDetector) GetChangesSummary(since time.Time, maxChanges int) string {
	changes := d.GetRecentChanges(maxChanges, since)
	if len(changes) == 0 {
		return ""
	}

	var result string
	for _, c := range changes {
		ago := time.Since(c.DetectedAt)
		result += "- " + c.Description + " (" + formatDuration(ago) + " ago)\n"
	}
	return result
}

// trimChanges removes old changes beyond maxChanges
func (d *ChangeDetector) trimChanges() {
	if len(d.changes) > d.maxChanges {
		// Keep most recent
		d.changes = d.changes[len(d.changes)-d.maxChanges:]
	}
}

// saveToDisk persists changes to JSON file
func (d *ChangeDetector) saveToDisk() error {
	if d.dataDir == "" {
		return nil
	}

	d.mu.RLock()
	changes := make([]Change, len(d.changes))
	copy(changes, d.changes)
	d.mu.RUnlock()

	path := filepath.Join(d.dataDir, "ai_changes.json")
	data, err := json.MarshalIndent(changes, "", "  ")
	if err != nil {
		return err
	}

	tmpPath := path + ".tmp"
	if err := os.WriteFile(tmpPath, data, 0600); err != nil {
		return err
	}

	return os.Rename(tmpPath, path)
}

// loadFromDisk loads changes from JSON file
func (d *ChangeDetector) loadFromDisk() error {
	if d.dataDir == "" {
		return nil
	}

	path := filepath.Join(d.dataDir, "ai_changes.json")
	if st, err := os.Stat(path); err == nil {
		const maxOnDiskBytes = 10 << 20 // 10 MiB safety cap
		if st.Size() > maxOnDiskBytes {
			return fmt.Errorf("change history file too large (%d bytes)", st.Size())
		}
	}
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}

	var changes []Change
	if err := json.Unmarshal(data, &changes); err != nil {
		return err
	}

	// Sort by time
	sort.Slice(changes, func(i, j int) bool {
		return changes[i].DetectedAt.Before(changes[j].DetectedAt)
	})

	d.changes = changes
	d.trimChanges()
	return nil
}

// Helper functions

var changeCounter int64

func generateChangeID() string {
	changeCounter++
	return time.Now().Format("20060102150405") + "-" + intToString(int(changeCounter%1000))
}

func formatDuration(d time.Duration) string {
	if d < time.Minute {
		return "just now"
	}
	if d < time.Hour {
		return formatUnit(int(d.Minutes()), "minute")
	}
	if d < 24*time.Hour {
		return formatUnit(int(d.Hours()), "hour")
	}
	return formatUnit(int(d.Hours()/24), "day")
}

func formatUnit(n int, unit string) string {
	if n == 1 {
		return "1 " + unit
	}
	return intToString(n) + " " + unit + "s"
}

func formatBytes(bytes int64) string {
	const (
		KB = 1024
		MB = KB * 1024
		GB = MB * 1024
	)
	switch {
	case bytes >= GB:
		return formatFloat(float64(bytes)/GB) + " GB"
	case bytes >= MB:
		return formatFloat(float64(bytes)/MB) + " MB"
	case bytes >= KB:
		return formatFloat(float64(bytes)/KB) + " KB"
	default:
		return intToString(int(bytes)) + " B"
	}
}

func formatFloat(v float64) string {
	// Simple formatting without fmt dependency
	whole := int(v)
	frac := int((v - float64(whole)) * 10)
	if frac == 0 {
		return intToString(whole)
	}
	return intToString(whole) + "." + string(rune('0'+frac))
}

func intToString(n int) string {
	if n == 0 {
		return "0"
	}
	var result string
	for n > 0 {
		result = string(rune('0'+n%10)) + result
		n /= 10
	}
	return result
}

func formatCreateDescription(r ResourceSnapshot) string {
	return r.Type + " '" + r.Name + "' created"
}

func formatDeleteDescription(r ResourceSnapshot) string {
	return r.Type + " '" + r.Name + "' deleted"
}

func formatStatusDescription(name, before, after string) string {
	return "'" + name + "' status changed: " + before + " → " + after
}

func formatMigrationDescription(name, fromNode, toNode string) string {
	return "'" + name + "' migrated from " + fromNode + " to " + toNode
}

func formatCPUChangeDescription(name string, before, after int) string {
	direction := "increased"
	if after < before {
		direction = "decreased"
	}
	return "'" + name + "' CPU " + direction + ": " + intToString(before) + " → " + intToString(after) + " cores"
}

func formatMemoryChangeDescription(name string, before, after int64) string {
	direction := "increased"
	if after < before {
		direction = "decreased"
	}
	return "'" + name + "' memory " + direction + ": " + formatBytes(before) + " → " + formatBytes(after)
}

func formatBackupDescription(name string, backupTime time.Time) string {
	return "'" + name + "' backup completed at " + backupTime.Format("2006-01-02 15:04")
}
