package handlers

import (
	"archive/zip"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net"
	"net/http"
	"os"
	"runtime"
	"strconv"
	"time"

	"git.mchus.pro/mchus/quoteforge/internal/appmeta"
	"git.mchus.pro/mchus/quoteforge/internal/db"
	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	syncsvc "git.mchus.pro/mchus/quoteforge/internal/services/sync"
	"github.com/gin-gonic/gin"
)

type SupportBundleHandler struct {
	localDB     *localdb.LocalDB
	connMgr     *db.ConnectionManager
	syncService *syncsvc.Service
	logFilePath string
}

func NewSupportBundleHandler(local *localdb.LocalDB, connMgr *db.ConnectionManager, svc *syncsvc.Service, logFilePath string) *SupportBundleHandler {
	return &SupportBundleHandler{
		localDB:     local,
		connMgr:     connMgr,
		syncService: svc,
		logFilePath: logFilePath,
	}
}

// DownloadBundle collects diagnostic data and streams a ZIP archive.
// GET /api/support-bundle
func (h *SupportBundleHandler) DownloadBundle(c *gin.Context) {
	now := time.Now().UTC()
	hostname, err := os.Hostname()
	if err != nil {
		slog.Warn("support bundle: could not get hostname", "err", err)
	}

	c.Header("Content-Type", "application/zip")
	c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="qfs-bundle-%s.zip"`, now.Format("20060102-150405")))

	zw := zip.NewWriter(c.Writer)
	defer zw.Close()

	writeJSON := func(name string, v any) {
		w, err := zw.Create(name)
		if err != nil {
			return
		}
		enc := json.NewEncoder(w)
		enc.SetIndent("", "  ")
		_ = enc.Encode(v)
	}

	// app_info.json
	writeJSON("app_info.json", map[string]any{
		"app_version":  appmeta.Version(),
		"go_version":   runtime.Version(),
		"os":           runtime.GOOS,
		"arch":         runtime.GOARCH,
		"hostname":     hostname,
		"db_user":      h.localDB.GetDBUser(),
		"collected_at": now.Format(time.RFC3339),
	})

	// local_db_stats.json
	writeJSON("local_db_stats.json", map[string]any{
		"components":                   h.localDB.CountLocalComponents(),
		"configurations":               h.localDB.CountConfigurations(),
		"projects":                     h.localDB.CountProjects(),
		"pricelists":                   h.localDB.CountLocalPricelists(),
		"pending_changes":              h.localDB.GetPendingCount(),
		"db_size_bytes":                h.localDB.DBFileSizeBytes(),
		"last_pricelist_sync_time":     h.localDB.GetLastSyncTime(),
		"last_pricelist_attempt":       h.localDB.GetLastPricelistSyncAttemptAt(),
		"last_pricelist_status":        h.localDB.GetLastPricelistSyncStatus(),
		"last_pricelist_error":         h.localDB.GetLastPricelistSyncError(),
		"last_component_sync_attempt":  h.localDB.GetLastComponentSyncAttemptAt(),
		"last_component_sync_status":   h.localDB.GetLastComponentSyncStatus(),
		"last_component_sync_error":    h.localDB.GetLastComponentSyncError(),
	})

	// db_connection.json — includes TCP ping to DB host
	connStatus := h.connMgr.GetStatus()
	dbConnDoc := map[string]any{
		"is_connected": connStatus.IsConnected,
		"last_error":   connStatus.LastError,
	}
	if settings, err := h.localDB.GetSettings(); err == nil && settings.Host != "" {
		addr := net.JoinHostPort(settings.Host, strconv.Itoa(settings.Port))
		start := time.Now()
		conn, dialErr := net.DialTimeout("tcp", addr, 3*time.Second)
		pingMs := time.Since(start).Milliseconds()
		if dialErr == nil {
			conn.Close()
			dbConnDoc["tcp_ping_ms"] = pingMs
			dbConnDoc["tcp_ping_addr"] = addr
		} else {
			dbConnDoc["tcp_ping_error"] = dialErr.Error()
			dbConnDoc["tcp_ping_addr"] = addr
		}
	}
	writeJSON("db_connection.json", dbConnDoc)

	// sync_readiness.json
	if h.syncService != nil {
		readiness, err := h.syncService.GetReadiness()
		if err != nil {
			writeJSON("sync_readiness.json", map[string]any{"error": err.Error()})
		} else {
			writeJSON("sync_readiness.json", readiness)
		}
	}

	// system_metrics.json
	writeJSON("system_metrics.json", collectSystemMetrics())

	// sync_log.json — history of sync operations
	if entries, err := h.localDB.GetSyncLog(200); err == nil {
		writeJSON("sync_log.json", entries)
	}

	// pricelists.json — downloaded pricelists grouped by source
	if pricelists, err := h.localDB.GetLocalPricelists(); err == nil {
		type plEntry struct {
			ServerID  uint      `json:"server_id"`
			Source    string    `json:"source"`
			Version   string    `json:"version"`
			Name      string    `json:"name,omitempty"`
			CreatedAt time.Time `json:"created_at"`
			SyncedAt  time.Time `json:"synced_at"`
			IsUsed    bool      `json:"is_used"`
		}
		bySource := map[string][]plEntry{}
		for _, pl := range pricelists {
			e := plEntry{
				ServerID:  pl.ServerID,
				Source:    pl.Source,
				Version:   pl.Version,
				Name:      pl.Name,
				CreatedAt: pl.CreatedAt,
				SyncedAt:  pl.SyncedAt,
				IsUsed:    pl.IsUsed,
			}
			bySource[pl.Source] = append(bySource[pl.Source], e)
		}
		writeJSON("pricelists.json", bySource)
	}

	// schema_migrations.json
	migrations, err := h.localDB.GetSchemaMigrations()
	if err != nil {
		slog.Warn("support bundle: could not load schema migrations", "err", err)
	}
	writeJSON("schema_migrations.json", migrations)

	// app.log (tail 5 MiB)
	if h.logFilePath != "" {
		if f, err := os.Open(h.logFilePath); err == nil {
			defer f.Close()
			if info, err := f.Stat(); err == nil {
				const maxLog = 5 << 20
				offset := int64(0)
				if info.Size() > maxLog {
					offset = info.Size() - maxLog
				}
				if _, err := f.Seek(offset, io.SeekStart); err == nil {
					if w, err := zw.Create("app.log"); err == nil {
						if _, err := io.Copy(w, f); err != nil {
							slog.Warn("support bundle: error copying log file", "err", err)
						}
					}
				}
			}
		}
	}

	c.Status(http.StatusOK)
}

func collectSystemMetrics() map[string]any {
	var ms runtime.MemStats
	runtime.ReadMemStats(&ms)

	m := map[string]any{
		"goroutines":        runtime.NumGoroutine(),
		"cpu_count":         runtime.NumCPU(),
		"heap_alloc_bytes":  ms.HeapAlloc,
		"heap_sys_bytes":    ms.HeapSys,
		"heap_inuse_bytes":  ms.HeapInuse,
		"stack_inuse_bytes": ms.StackInuse,
		"gc_cycles":         ms.NumGC,
		"next_gc_bytes":     ms.NextGC,
	}

	if wd, err := os.Getwd(); err == nil {
		if info := diskUsage(wd); info != nil {
			m["disk"] = info
		}
	}

	return m
}
