package handlers

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"armature/database"
	"armature/store"
)

// BackupHandler provides backup/restore endpoints for admin users.
type BackupHandler struct {
	stores      store.Stores
	packagesDir string
	storagePath string
}

// NewBackupHandler creates a new BackupHandler.
func NewBackupHandler(stores store.Stores, packagesDir, storagePath string) *BackupHandler {
	return &BackupHandler{
		stores:      stores,
		packagesDir: packagesDir,
		storagePath: storagePath,
	}
}

// backupMeta is written as meta.json inside the .swb archive.
type backupMeta struct {
	Version       string    `json:"version"`
	Timestamp     time.Time `json:"timestamp"`
	SchemaVersion string    `json:"schema_version"`
	Dialect       string    `json:"dialect"`
	CoreTables    []string  `json:"core_tables"`
	ExtTables     []string  `json:"ext_tables,omitempty"`
}

func (h *BackupHandler) backupsDir() string {
	if h.storagePath == "" {
		return ""
	}
	return filepath.Join(h.storagePath, "backups")
}

// ── CreateBackup ─────────────────────────────
// POST /api/v1/admin/backup
// Streams a .swb ZIP archive containing all core + ext_data tables as JSONL,
// plus package asset files from disk.

func (h *BackupHandler) CreateBackup(c *gin.Context) {
	ctx := c.Request.Context()
	db := database.DB

	// Determine schema version
	schemaVersion := ""
	row := db.QueryRowContext(ctx, "SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1")
	row.Scan(&schemaVersion)

	dialect := "sqlite"
	if database.IsPostgres() {
		dialect = "postgres"
	}

	// Enumerate ext_data tables
	extEntries, err := listExtDataTables(ctx, db)
	if err != nil {
		log.Printf("[backup] warning: list ext_data tables: %v", err)
	}

	extTableNames := make([]string, len(extEntries))
	for i, e := range extEntries {
		extTableNames[i] = e.PhysicalName
	}

	meta := backupMeta{
		Version:       readVersionFile(),
		Timestamp:     time.Now().UTC(),
		SchemaVersion: schemaVersion,
		Dialect:       dialect,
		CoreTables:    coreTableOrder,
		ExtTables:     extTableNames,
	}

	// Determine if we should store server-side or stream directly
	storeSide := c.Query("store") == "true"

	if storeSide {
		h.createStoredBackup(c, ctx, db, meta, extEntries)
		return
	}

	// Stream directly to client
	filename := fmt.Sprintf("armature-%s-%s.swb", meta.Version, meta.Timestamp.Format("20060102-150405"))
	c.Header("Content-Type", "application/zip")
	c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))

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

	h.writeBackupContents(c, zw, meta, extEntries)
}

func (h *BackupHandler) createStoredBackup(c *gin.Context, ctx interface{ Deadline() (time.Time, bool) }, db interface{}, meta backupMeta, extEntries []extDataTableEntry) {
	dir := h.backupsDir()
	if dir == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "no storage path configured"})
		return
	}
	os.MkdirAll(dir, 0755)

	filename := fmt.Sprintf("armature-%s-%s.swb", meta.Version, meta.Timestamp.Format("20060102-150405"))
	path := filepath.Join(dir, filename)

	f, err := os.Create(path)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "create backup file: " + err.Error()})
		return
	}
	defer f.Close()

	zw := zip.NewWriter(f)
	defer zw.Close()

	h.writeBackupContents(c, zw, meta, extEntries)

	c.JSON(http.StatusOK, gin.H{"data": gin.H{
		"filename": filename,
		"path":     path,
	}})
}

func (h *BackupHandler) writeBackupContents(c *gin.Context, zw *zip.Writer, meta backupMeta, extEntries []extDataTableEntry) {
	ctx := c.Request.Context()
	db := database.DB

	// Write meta.json
	mw, err := zw.Create("meta.json")
	if err != nil {
		log.Printf("[backup] create meta.json: %v", err)
		return
	}
	enc := json.NewEncoder(mw)
	enc.SetIndent("", "  ")
	enc.Encode(meta)

	// Dump core tables
	for _, table := range coreTableOrder {
		if !tableExists(ctx, db, table) {
			continue
		}
		fw, err := zw.Create(fmt.Sprintf("core/%s.jsonl", table))
		if err != nil {
			log.Printf("[backup] create core/%s.jsonl: %v", table, err)
			continue
		}
		count, err := dumpTable(ctx, db, table, fw)
		if err != nil {
			log.Printf("[backup] dump %s: %v", table, err)
		} else {
			log.Printf("[backup] dumped %s: %d rows", table, count)
		}
	}

	// Dump ext_data tables
	for _, entry := range extEntries {
		if !tableExists(ctx, db, entry.PhysicalName) {
			continue
		}
		path := fmt.Sprintf("ext_data/%s/%s.jsonl", entry.PackageID, entry.TableName)
		fw, err := zw.Create(path)
		if err != nil {
			log.Printf("[backup] create %s: %v", path, err)
			continue
		}
		count, err := dumpTable(ctx, db, entry.PhysicalName, fw)
		if err != nil {
			log.Printf("[backup] dump %s: %v", entry.PhysicalName, err)
		} else {
			log.Printf("[backup] dumped %s: %d rows", entry.PhysicalName, count)
		}
	}

	// Walk package assets
	if h.packagesDir != "" {
		filepath.Walk(h.packagesDir, func(path string, info os.FileInfo, err error) error {
			if err != nil || info.IsDir() {
				return nil
			}
			relPath, err := filepath.Rel(h.packagesDir, path)
			if err != nil {
				return nil
			}
			relPath = filepath.ToSlash(relPath)
			fw, err := zw.Create("packages/" + relPath)
			if err != nil {
				return nil
			}
			f, err := os.Open(path)
			if err != nil {
				return nil
			}
			defer f.Close()
			io.Copy(fw, f)
			return nil
		})
	}
}

// ── ListBackups ──────────────────────────────
// GET /api/v1/admin/backups

func (h *BackupHandler) ListBackups(c *gin.Context) {
	dir := h.backupsDir()
	if dir == "" {
		c.JSON(http.StatusOK, gin.H{"data": []interface{}{}})
		return
	}

	entries, err := os.ReadDir(dir)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"data": []interface{}{}})
		return
	}

	type backupEntry struct {
		Name      string    `json:"name"`
		Size      int64     `json:"size"`
		CreatedAt time.Time `json:"created_at"`
	}

	var backups []backupEntry
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".swb") {
			continue
		}
		info, err := e.Info()
		if err != nil {
			continue
		}
		backups = append(backups, backupEntry{
			Name:      e.Name(),
			Size:      info.Size(),
			CreatedAt: info.ModTime(),
		})
	}
	if backups == nil {
		backups = []backupEntry{}
	}

	c.JSON(http.StatusOK, gin.H{"data": backups})
}

// ── DownloadBackup ───────────────────────────
// GET /api/v1/admin/backups/:name

func (h *BackupHandler) DownloadBackup(c *gin.Context) {
	name := c.Param("name")
	if !isValidBackupName(name) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid backup name"})
		return
	}

	dir := h.backupsDir()
	if dir == "" {
		c.JSON(http.StatusNotFound, gin.H{"error": "no storage configured"})
		return
	}

	path := filepath.Join(dir, name)
	if _, err := os.Stat(path); os.IsNotExist(err) {
		c.JSON(http.StatusNotFound, gin.H{"error": "backup not found"})
		return
	}

	c.Header("Content-Type", "application/zip")
	c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", name))
	c.File(path)
}

// ── DeleteBackup ─────────────────────────────
// DELETE /api/v1/admin/backups/:name

func (h *BackupHandler) DeleteBackup(c *gin.Context) {
	name := c.Param("name")
	if !isValidBackupName(name) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid backup name"})
		return
	}

	dir := h.backupsDir()
	if dir == "" {
		c.JSON(http.StatusNotFound, gin.H{"error": "no storage configured"})
		return
	}

	path := filepath.Join(dir, name)
	if err := os.Remove(path); err != nil {
		if os.IsNotExist(err) {
			c.JSON(http.StatusNotFound, gin.H{"error": "backup not found"})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		}
		return
	}

	c.JSON(http.StatusOK, gin.H{"data": gin.H{"deleted": name}})
}

// ── RestoreBackup ────────────────────────────
// POST /api/v1/admin/restore
// Accepts a .swb file upload, validates meta, wipes DB, restores data.

func (h *BackupHandler) RestoreBackup(c *gin.Context) {
	ctx := c.Request.Context()
	db := database.DB

	file, header, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "file required"})
		return
	}
	defer file.Close()

	// Read entire file into memory for zip.NewReader
	data, err := io.ReadAll(file)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "read file: " + err.Error()})
		return
	}

	zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid archive: " + err.Error()})
		return
	}

	// Read meta.json
	meta, err := readBackupMeta(zr)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid backup: " + err.Error()})
		return
	}

	// Validate schema version — reject backups from future schema
	currentSchema := ""
	db.QueryRowContext(ctx, "SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1").Scan(&currentSchema)
	if meta.SchemaVersion > currentSchema && currentSchema != "" {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": fmt.Sprintf("backup schema %s is newer than current %s — upgrade the application first", meta.SchemaVersion, currentSchema),
		})
		return
	}

	log.Printf("[backup] restoring from %s (schema %s, %d core tables, %d ext tables)",
		header.Filename, meta.SchemaVersion, len(meta.CoreTables), len(meta.ExtTables))

	// Build full table list for wipe (core + ext_data)
	allTables := make([]string, 0, len(coreTableOrder)+len(meta.ExtTables))
	allTables = append(allTables, coreTableOrder...)
	allTables = append(allTables, meta.ExtTables...)

	// Wipe all data
	if err := wipeTables(ctx, db, allTables); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "wipe failed: " + err.Error()})
		return
	}

	// Restore core tables in order
	restored := 0
	for _, table := range coreTableOrder {
		path := fmt.Sprintf("core/%s.jsonl", table)
		f := findInZip(zr, path)
		if f == nil {
			continue
		}
		rc, err := f.Open()
		if err != nil {
			log.Printf("[backup] open %s: %v", path, err)
			continue
		}
		count, err := restoreTable(ctx, db, table, rc)
		rc.Close()
		if err != nil {
			log.Printf("[backup] restore %s: %v", table, err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("restore %s: %v", table, err)})
			return
		}
		log.Printf("[backup] restored %s: %d rows", table, count)
		restored += count
	}

	// Restore ext_data tables
	for _, extTable := range meta.ExtTables {
		// Parse package_id and table_name from physical name: ext_{pkg}_{table}
		pkgID, tblName := parseExtTableName(extTable)
		if pkgID == "" {
			continue
		}
		path := fmt.Sprintf("ext_data/%s/%s.jsonl", pkgID, tblName)
		f := findInZip(zr, path)
		if f == nil {
			continue
		}
		// Ensure physical table exists before restoring
		if !tableExists(ctx, db, extTable) {
			log.Printf("[backup] skip ext table %s (not in schema)", extTable)
			continue
		}
		rc, err := f.Open()
		if err != nil {
			log.Printf("[backup] open %s: %v", path, err)
			continue
		}
		count, err := restoreTable(ctx, db, extTable, rc)
		rc.Close()
		if err != nil {
			log.Printf("[backup] restore ext %s: %v", extTable, err)
		} else {
			log.Printf("[backup] restored ext %s: %d rows", extTable, count)
			restored += count
		}
	}

	// Restore package files
	if h.packagesDir != "" {
		for _, f := range zr.File {
			if !strings.HasPrefix(f.Name, "packages/") || f.FileInfo().IsDir() {
				continue
			}
			relPath := strings.TrimPrefix(f.Name, "packages/")
			destPath := filepath.Join(h.packagesDir, filepath.FromSlash(relPath))
			os.MkdirAll(filepath.Dir(destPath), 0755)
			rc, err := f.Open()
			if err != nil {
				continue
			}
			out, err := os.Create(destPath)
			if err != nil {
				rc.Close()
				continue
			}
			io.Copy(out, rc)
			out.Close()
			rc.Close()
		}
	}

	log.Printf("[backup] restore complete: %d total rows", restored)
	c.JSON(http.StatusOK, gin.H{"data": gin.H{
		"rows_restored": restored,
		"schema":        meta.SchemaVersion,
	}})
}

// ── Helpers ──────────────────────────────────

func readBackupMeta(zr *zip.Reader) (*backupMeta, error) {
	f := findInZip(zr, "meta.json")
	if f == nil {
		return nil, fmt.Errorf("meta.json not found in archive")
	}
	rc, err := f.Open()
	if err != nil {
		return nil, err
	}
	defer rc.Close()
	var meta backupMeta
	if err := json.NewDecoder(rc).Decode(&meta); err != nil {
		return nil, fmt.Errorf("decode meta.json: %w", err)
	}
	return &meta, nil
}

func findInZip(zr *zip.Reader, name string) *zip.File {
	for _, f := range zr.File {
		if f.Name == name {
			return f
		}
	}
	return nil
}

// parseExtTableName parses "ext_{pkg}_{table}" into (pkg, table).
func parseExtTableName(physical string) (string, string) {
	if !strings.HasPrefix(physical, "ext_") {
		return "", ""
	}
	rest := physical[4:]
	idx := strings.Index(rest, "_")
	if idx < 0 {
		return "", ""
	}
	return rest[:idx], rest[idx+1:]
}

var validBackupNameRe = regexp.MustCompile(`^[a-zA-Z0-9._-]+\.swb$`)

func isValidBackupName(name string) bool {
	return validBackupNameRe.MatchString(name) && !strings.Contains(name, "..")
}

// readVersionFile reads the VERSION file from the project root.
func readVersionFile() string {
	// Try common locations
	for _, path := range []string{"VERSION", "/app/VERSION", "../VERSION"} {
		data, err := os.ReadFile(path)
		if err == nil {
			return strings.TrimSpace(string(data))
		}
	}
	return "unknown"
}
