package services

import (
	"archive/zip"
	"fmt"
	"io"
	"math"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"syscall"
	"time"

	"github.com/Wikid82/charon/backend/internal/config"
	"github.com/Wikid82/charon/backend/internal/logger"
	"github.com/robfig/cron/v3"
)

type BackupService struct {
	DataDir      string
	BackupDir    string
	DatabaseName string
	Cron         *cron.Cron
}

type BackupFile struct {
	Filename string    `json:"filename"`
	Size     int64     `json:"size"`
	Time     time.Time `json:"time"`
}

func NewBackupService(cfg *config.Config) *BackupService {
	// Ensure backup directory exists
	backupDir := filepath.Join(filepath.Dir(cfg.DatabasePath), "backups")
	if err := os.MkdirAll(backupDir, 0o755); err != nil {
		logger.Log().WithError(err).Error("Failed to create backup directory")
	}

	s := &BackupService{
		DataDir:      filepath.Dir(cfg.DatabasePath), // e.g. /app/data
		BackupDir:    backupDir,
		DatabaseName: filepath.Base(cfg.DatabasePath),
		Cron:         cron.New(),
	}

	// Schedule daily backup at 3 AM
	_, err := s.Cron.AddFunc("0 3 * * *", s.RunScheduledBackup)
	if err != nil {
		logger.Log().WithError(err).Error("Failed to schedule backup")
	}
	s.Cron.Start()

	return s
}

func (s *BackupService) RunScheduledBackup() {
	logger.Log().Info("Starting scheduled backup")
	if name, err := s.CreateBackup(); err != nil {
		logger.Log().WithError(err).Error("Scheduled backup failed")
	} else {
		logger.Log().WithField("backup", name).Info("Scheduled backup created")
	}
}

// ListBackups returns all backup files sorted by time (newest first)
func (s *BackupService) ListBackups() ([]BackupFile, error) {
	entries, err := os.ReadDir(s.BackupDir)
	if err != nil {
		if os.IsNotExist(err) {
			return []BackupFile{}, nil
		}
		return nil, err
	}

	var backups []BackupFile
	for _, entry := range entries {
		if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".zip") {
			info, err := entry.Info()
			if err != nil {
				continue
			}
			backups = append(backups, BackupFile{
				Filename: entry.Name(),
				Size:     info.Size(),
				Time:     info.ModTime(),
			})
		}
	}

	// Sort newest first
	sort.Slice(backups, func(i, j int) bool {
		return backups[i].Time.After(backups[j].Time)
	})

	return backups, nil
}

// CreateBackup creates a zip archive of the database and caddy data
func (s *BackupService) CreateBackup() (string, error) {
	timestamp := time.Now().Format("2006-01-02_15-04-05")
	filename := fmt.Sprintf("backup_%s.zip", timestamp)
	zipPath := filepath.Join(s.BackupDir, filename)

	outFile, err := os.Create(zipPath)
	if err != nil {
		return "", err
	}
	defer func() {
		if err := outFile.Close(); err != nil {
			logger.Log().WithError(err).Warn("failed to close backup file")
		}
	}()

	w := zip.NewWriter(outFile)

	// Files/Dirs to backup
	// 1. Database
	dbPath := filepath.Join(s.DataDir, s.DatabaseName)
	// Ensure DB exists before backing up
	if _, err := os.Stat(dbPath); os.IsNotExist(err) {
		return "", fmt.Errorf("database file not found: %s", dbPath)
	}
	if err := s.addToZip(w, dbPath, s.DatabaseName); err != nil {
		return "", fmt.Errorf("backup db: %w", err)
	}

	// 2. Caddy Data (Certificates, etc)
	// We walk the 'caddy' subdirectory
	caddyDir := filepath.Join(s.DataDir, "caddy")
	if err := s.addDirToZip(w, caddyDir, "caddy"); err != nil {
		// It's possible caddy dir doesn't exist yet, which is fine
		logger.Log().WithError(err).Warn("Warning: could not backup caddy dir")
	}

	// Close zip writer and check for errors (important for zip integrity)
	if err := w.Close(); err != nil {
		return "", fmt.Errorf("failed to finalize backup: %w", err)
	}

	return filename, nil
}

func (s *BackupService) addToZip(w *zip.Writer, srcPath, zipPath string) error {
	file, err := os.Open(srcPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}
	defer func() {
		if err := file.Close(); err != nil {
			logger.Log().WithError(err).Warn("failed to close file after adding to zip")
		}
	}()

	f, err := w.Create(zipPath)
	if err != nil {
		return err
	}

	_, err = io.Copy(f, file)
	return err
}

func (s *BackupService) addDirToZip(w *zip.Writer, srcDir, zipBase string) error {
	return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			return nil
		}

		relPath, err := filepath.Rel(srcDir, path)
		if err != nil {
			return err
		}

		zipPath := filepath.Join(zipBase, relPath)
		return s.addToZip(w, path, zipPath)
	})
}

// DeleteBackup removes a backup file
func (s *BackupService) DeleteBackup(filename string) error {
	cleanName := filepath.Base(filename)
	if filename != cleanName {
		return fmt.Errorf("invalid filename: path traversal attempt detected")
	}
	path := filepath.Join(s.BackupDir, cleanName)
	if !strings.HasPrefix(path, filepath.Clean(s.BackupDir)) {
		return fmt.Errorf("invalid filename: path traversal attempt detected")
	}
	return os.Remove(path)
}

// GetBackupPath returns the full path to a backup file (for downloading)
func (s *BackupService) GetBackupPath(filename string) (string, error) {
	cleanName := filepath.Base(filename)
	if filename != cleanName {
		return "", fmt.Errorf("invalid filename: path traversal attempt detected")
	}
	path := filepath.Join(s.BackupDir, cleanName)
	if !strings.HasPrefix(path, filepath.Clean(s.BackupDir)) {
		return "", fmt.Errorf("invalid filename: path traversal attempt detected")
	}
	return path, nil
}

// RestoreBackup restores the database and caddy data from a zip archive
func (s *BackupService) RestoreBackup(filename string) error {
	cleanName := filepath.Base(filename)
	if filename != cleanName {
		return fmt.Errorf("invalid filename: path traversal attempt detected")
	}
	// 1. Verify backup exists
	srcPath := filepath.Join(s.BackupDir, cleanName)
	if !strings.HasPrefix(srcPath, filepath.Clean(s.BackupDir)) {
		return fmt.Errorf("invalid filename: path traversal attempt detected")
	}
	if _, err := os.Stat(srcPath); err != nil {
		return err
	}

	// 2. Unzip to DataDir (overwriting)
	return s.unzip(srcPath, s.DataDir)
}

func (s *BackupService) unzip(src, dest string) error {
	r, err := zip.OpenReader(src)
	if err != nil {
		return err
	}
	defer func() {
		if err := r.Close(); err != nil {
			logger.Log().WithError(err).Warn("failed to close zip reader")
		}
	}()

	for _, f := range r.File {
		fpath := filepath.Join(dest, f.Name)

		// Check for ZipSlip
		if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
			return fmt.Errorf("illegal file path: %s", fpath)
		}

		if f.FileInfo().IsDir() {
			_ = os.MkdirAll(fpath, os.ModePerm)
			continue
		}

		if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
			return err
		}

		outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
		if err != nil {
			return err
		}

		rc, err := f.Open()
		if err != nil {
			if err := outFile.Close(); err != nil {
				logger.Log().WithError(err).Warn("failed to close temporary output file after f.Open() error")
			}
			return err
		}

		_, err = io.Copy(outFile, rc)

		// Check for close errors on writable file
		if closeErr := outFile.Close(); closeErr != nil && err == nil {
			err = closeErr
		}
		rc.Close()

		if err != nil {
			return err
		}
	}
	return nil
}

// GetAvailableSpace returns the available disk space in bytes for the backup directory
func (s *BackupService) GetAvailableSpace() (int64, error) {
	var stat syscall.Statfs_t
	if err := syscall.Statfs(s.BackupDir, &stat); err != nil {
		return 0, fmt.Errorf("failed to get disk space: %w", err)
	}

	// Safe conversion with overflow protection (gosec G115)
	bsize := stat.Bsize
	bavail := stat.Bavail

	// Check for invalid filesystem (negative block size)
	if bsize < 0 {
		return 0, fmt.Errorf("invalid block size: %d", bsize)
	}

	// Check if bavail exceeds max int64 before conversion
	if bavail > uint64(math.MaxInt64) {
		return math.MaxInt64, nil
	}

	// Safe to convert now
	availBlocks := int64(bavail)
	blockSize := int64(bsize)

	// Check for multiplication overflow
	if availBlocks > 0 && blockSize > math.MaxInt64/availBlocks {
		return math.MaxInt64, nil
	}

	return availBlocks * blockSize, nil
}
