package report

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"log"
	"os"
	"time"

	"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
	"gitea.dooplex.hu/admin/felhom-controller/internal/config"
	"gitea.dooplex.hu/admin/felhom-controller/internal/metrics"
	"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
	"gitea.dooplex.hu/admin/felhom-controller/internal/scheduler"
	"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
	"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
	"gitea.dooplex.hu/admin/felhom-controller/internal/system"
)

// BuildReport collects current state from all subsystems and returns a Report.
func BuildReport(
	cfg *config.Config,
	configPath string,
	stackMgr *stacks.Manager,
	backupMgr *backup.Manager,
	cpuCollector *system.CPUCollector,
	metricsStore *metrics.MetricsStore,
	version string,
	storagePaths []settings.StoragePath,
	geoRestriction *settings.GeoRestriction,
	logger *log.Logger,
) *Report {
	debug := cfg.Logging.Level == "debug"
	if logger != nil {
		logger.Printf("[INFO] [report] Building system report")
	}
	if debug && logger != nil {
		logger.Printf("[DEBUG] [report] BuildReport: starting — version=%s, storagePaths=%d", version, len(storagePaths))
	}

	r := &Report{
		Version:           1,
		CustomerID:        cfg.Customer.ID,
		CustomerName:      cfg.Customer.Name,
		ControllerVersion: version,
		Timestamp:         time.Now().UTC(),
	}

	// Controller URL for hub callbacks (self-update trigger, etc.)
	if cfg.Customer.Domain != "" {
		r.ControllerURL = fmt.Sprintf("https://felhom.%s", cfg.Customer.Domain)
	}

	// Config hash for Hub comparison
	if configPath != "" {
		if data, err := os.ReadFile(configPath); err == nil {
			h := sha256.Sum256(data)
			r.ConfigHash = hex.EncodeToString(h[:])
			if debug && logger != nil {
				logger.Printf("[DEBUG] [report] BuildReport: configHash=%s (%d bytes)", r.ConfigHash[:12]+"...", len(data))
			}
		}
	}

	// System info
	staticInfo := metrics.GetStaticInfo()
	hddPath := cfg.Paths.HDDPath
	if len(storagePaths) > 0 {
		hddPath = storagePaths[0].Path
	}
	sysInfo := system.GetInfo(hddPath, cpuCollector)

	r.System = SystemReport{
		Hostname:           staticInfo.Hostname,
		OS:                 staticInfo.OS,
		Kernel:             staticInfo.Kernel,
		CPUModel:           staticInfo.CPUModel,
		CPUCores:           staticInfo.CPUCores,
		UptimeSeconds:      staticInfo.UptimeSeconds,
		CPUPercent:         sysInfo.CPUPercent,
		MemoryTotalMB:      sysInfo.TotalMemMB,
		MemoryUsedMB:       sysInfo.UsedMemMB,
		MemoryPercent:      sysInfo.MemPercent,
		TemperatureCelsius: sysInfo.TemperatureCelsius,
		LoadAvg1:           sysInfo.LoadAvg1,
		LoadAvg5:           sysInfo.LoadAvg5,
		LoadAvg15:          sysInfo.LoadAvg15,
	}

	// Storage — root filesystem + all registered storage paths
	r.Storage = []StorageReport{
		{Mount: "/", Label: "SSD", TotalGB: sysInfo.DiskTotalGB, UsedGB: sysInfo.DiskUsedGB, Percent: sysInfo.DiskPercent},
	}
	for _, sp := range storagePaths {
		if sp.Decommissioned {
			r.Storage = append(r.Storage, StorageReport{
				Mount:          sp.Path,
				Label:          sp.Label,
				Decommissioned: true,
				MigratedTo:     sp.MigratedTo,
			})
			continue
		}
		if sp.Disconnected {
			r.Storage = append(r.Storage, StorageReport{
				Mount:        sp.Path,
				Label:        sp.Label,
				Disconnected: true,
			})
			continue
		}
		di := system.GetDiskUsage(sp.Path)
		if di == nil {
			continue
		}
		r.Storage = append(r.Storage, StorageReport{
			Mount:   sp.Path,
			Label:   sp.Label,
			TotalGB: di.TotalGB,
			UsedGB:  di.UsedGB,
			Percent: di.UsedPercent,
		})
	}

	if debug && logger != nil {
		logger.Printf("[DEBUG] [report] BuildReport: system info collected — cpu=%.1f%%, mem=%d/%dMB, temp=%.1fC",
			sysInfo.CPUPercent, sysInfo.UsedMemMB, sysInfo.TotalMemMB, sysInfo.TemperatureCelsius)
		logger.Printf("[DEBUG] [report] BuildReport: storage entries=%d", len(r.Storage))
	}

	// Containers
	r.Containers = buildContainerReport(stackMgr, metricsStore)

	// Backup
	r.Backup = buildBackupReport(cfg, backupMgr)

	// Health
	healthReport := monitor.RunHealthCheck(cfg, cpuCollector, storagePaths, logger)
	r.Health = HealthReport{
		Status:   healthReport.Status,
		Issues:   healthReport.Issues,
		Warnings: healthReport.Warnings,
	}
	if r.Health.Issues == nil {
		r.Health.Issues = []string{}
	}
	if r.Health.Warnings == nil {
		r.Health.Warnings = []string{}
	}

	// Stacks
	r.Stacks = buildStacksReport(stackMgr)

	// App telemetry (metrics + log scan)
	r.AppTelemetry = buildAppTelemetrySection(stackMgr, metricsStore, logger)

	// Geo-restriction status — ALWAYS present (even when never configured) so the hub
	// always renders the section. A nil pointer (omitempty) made the hub hide the whole
	// section for a never-configured controller; a present-but-disabled report renders
	// "Inaktív" hub-side.
	r.GeoRestriction = buildGeoRestrictionReport(geoRestriction)

	// DR recipe app-half — customer identity + per-app {catalog_ref, enabled, storage_bindings}.
	// Allowlist-only (the boundary): NO env/secret fields. The hub assembles it with the agent half.
	r.DRRecipe = BuildDRRecipeAppHalf(cfg.Customer.ID, cfg.Customer.Name, cfg.Customer.Domain,
		stackMgr.GetStacks(), readComposeFile)
	// fork-4: attach the non-secret offsite restic repo coordinates so DR knows where to recover from.
	if backupMgr != nil {
		if host, user, port, repoPath, ok := backupMgr.OffboxCoord(); ok {
			r.DRRecipe.OffsiteRestic = &DRResticCoord{Host: host, User: user, Port: port, RepoPath: repoPath}
		}
	}

	if debug && logger != nil {
		logger.Printf("[DEBUG] [report] BuildReport: complete — containers=%d, health=%s, deployed=%d, available=%d, app_telemetry=%d",
			r.Containers.Total, r.Health.Status, len(r.Stacks.Deployed), len(r.Stacks.Available), len(r.AppTelemetry))
	}

	return r
}

// buildGeoRestrictionReport always returns a non-nil report so the hub always renders the
// geo-restriction section. A nil or never-configured input yields Enabled=false with an
// empty (non-nil) country list — the hub renders that as "Inaktív".
func buildGeoRestrictionReport(geo *settings.GeoRestriction) *GeoRestrictionReport {
	gr := &GeoRestrictionReport{Enabled: false, AllowedCountries: []string{}}
	if geo == nil {
		return gr
	}
	gr.Enabled = geo.Enabled
	if geo.AllowedCountries != nil {
		gr.AllowedCountries = geo.AllowedCountries
	}
	gr.LastSync = geo.LastSync
	gr.LastSyncError = geo.LastSyncError
	if len(geo.AppOverrides) > 0 {
		gr.AppOverrides = make(map[string]GeoAppOverrideReport, len(geo.AppOverrides))
		for k, v := range geo.AppOverrides {
			gr.AppOverrides[k] = GeoAppOverrideReport{AllowedCountries: v.AllowedCountries}
		}
	}
	return gr
}

func buildContainerReport(stackMgr *stacks.Manager, metricsStore *metrics.MetricsStore) ContainerReport {
	cr := ContainerReport{}

	allStacks := stackMgr.GetStacks()

	// Build a map of container stats from metrics store
	statsMap := make(map[string]metrics.ContainerCurrentStats)
	if metricsStore != nil {
		if stats, err := metricsStore.QueryContainerSummary(); err == nil {
			for _, s := range stats {
				statsMap[s.ContainerName] = s
			}
		}
	}

	for _, s := range allStacks {
		if !s.Deployed {
			continue
		}
		for _, c := range s.Containers {
			cr.Total++
			switch c.State {
			case stacks.StateRunning, stacks.StateStarting:
				cr.Running++
			case stacks.StateUnhealthy:
				cr.Unhealthy++
				cr.Running++ // unhealthy containers are still running
			default:
				cr.Stopped++
			}

			detail := ContainerDetailReport{
				Name:  c.Name,
				State: string(c.State),
			}
			if cs, ok := statsMap[c.Name]; ok {
				detail.CPUPercent = cs.CPUPercent
				detail.MemoryMB = cs.MemUsageMB
			}
			cr.List = append(cr.List, detail)
		}
	}

	if cr.List == nil {
		cr.List = []ContainerDetailReport{}
	}

	return cr
}

func buildBackupReport(cfg *config.Config, backupMgr *backup.Manager) BackupReport {
	br := BackupReport{
		Enabled: cfg.Backup.Enabled,
	}

	if backupMgr == nil {
		return br
	}

	// Disk-tier backup (restic snapshots, integrity check, repo stats) has moved to
	// the host agent (slice 8C). The controller report now covers only app-data backup
	// (database dumps); restic/snapshot/integrity fields are left zero.
	nextDBDump := scheduler.NextDailyRun(cfg.Backup.DBDumpSchedule)
	status := backupMgr.GetFullStatus(nextDBDump)

	if status.LastDBDump != nil {
		t := status.LastDBDump.LastRun
		br.LastDBDump = &t
	}

	return br
}

func buildStacksReport(stackMgr *stacks.Manager) StacksReport {
	sr := StacksReport{}
	allStacks := stackMgr.GetStacks()

	for _, s := range allStacks {
		if s.Protected {
			continue
		}
		if s.Deployed {
			sr.Deployed = append(sr.Deployed, s.Name)
		} else {
			sr.Available = append(sr.Available, s.Name)
		}
	}

	if sr.Deployed == nil {
		sr.Deployed = []string{}
	}
	if sr.Available == nil {
		sr.Available = []string{}
	}

	return sr
}

