package daemonrunner

import (
	"context"
	"fmt"
	"io"
	"os"
	"os/signal"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/steveyegge/beads"
	"github.com/steveyegge/beads/internal/daemon"
	"github.com/steveyegge/beads/internal/rpc"
	"github.com/steveyegge/beads/internal/storage"
	"github.com/steveyegge/beads/internal/storage/sqlite"
	"gopkg.in/natefinch/lumberjack.v2"
)

// Daemon represents a running background daemon
type Daemon struct {
	cfg    Config
	log    *logger
	logF   *lumberjack.Logger
	store  storage.Storage
	server *rpc.Server
	lock   io.Closer
	cancel context.CancelFunc
	
	// Version is the daemon's build version
	Version string
}

// New creates a new Daemon instance
func New(cfg Config, version string) *Daemon {
	return &Daemon{
		cfg:     cfg,
		Version: version,
	}
}

// Start runs the daemon main loop
func (d *Daemon) Start() error {
	// Setup logger
	d.logF, d.log = d.setupLogger()
	defer func() { _ = d.logF.Close() }()

	// Determine database path for local daemon
	if !d.cfg.Global {
		if err := d.determineDatabasePath(); err != nil {
			return err
		}
	}

	// Acquire daemon lock
	lock, err := d.setupLock()
	if err != nil {
		return err
	}
	d.lock = lock
	defer func() { _ = d.lock.Close() }()
	defer func() { _ = os.Remove(d.cfg.PIDFile) }()

	d.log.log("Daemon started (interval: %v, auto-commit: %v, auto-push: %v)", 
		d.cfg.Interval, d.cfg.AutoCommit, d.cfg.AutoPush)

	// Handle global daemon differently
	if d.cfg.Global {
		return d.runGlobalDaemon()
	}

	// Validate single canonical database
	if err := d.validateSingleDatabase(); err != nil {
		return err
	}

	d.log.log("Using database: %s", d.cfg.DBPath)

	// Clear any previous daemon-error file on successful startup
	errFile := filepath.Join(d.cfg.BeadsDir, "daemon-error")
	if err := os.Remove(errFile); err != nil && !os.IsNotExist(err) {
		d.log.log("Warning: could not remove daemon-error file: %v", err)
	}

	// Open database
	store, err := sqlite.New(d.cfg.DBPath)
	if err != nil {
		d.log.log("Error: cannot open database: %v", err)
		return fmt.Errorf("cannot open database: %w", err)
	}
	d.store = store
	defer func() { _ = d.store.Close() }()
	d.log.log("Database opened: %s", d.cfg.DBPath)

	// Validate database fingerprint
	if err := d.validateDatabaseFingerprint(); err != nil {
		if os.Getenv("BEADS_IGNORE_REPO_MISMATCH") != "1" {
			d.log.log("Error: %v", err)
			return err
		}
		d.log.log("Warning: repository mismatch ignored (BEADS_IGNORE_REPO_MISMATCH=1)")
	}

	// Validate schema version
	if err := d.validateSchemaVersion(); err != nil {
		return err
	}

	// Start RPC server
	ctx, cancel := context.WithCancel(context.Background())
	d.cancel = cancel
	defer cancel()

	server, serverErrChan, err := d.startRPCServer(ctx)
	if err != nil {
		return err
	}
	d.server = server

	// Register in global registry
	if err := d.registerDaemon(); err != nil {
		d.log.log("Warning: failed to register daemon: %v", err)
	} else {
		defer d.unregisterDaemon()
	}

	// Run sync loops
	return d.runSyncLoop(ctx, serverErrChan)
}

func (d *Daemon) runGlobalDaemon() error {
	globalDir, err := getGlobalBeadsDir()
	if err != nil {
		d.log.log("Error: cannot get global beads directory: %v", err)
		return err
	}
	d.cfg.SocketPath = filepath.Join(globalDir, "bd.sock")

	ctx, cancel := context.WithCancel(context.Background())
	d.cancel = cancel
	defer cancel()

	server, _, err := d.startRPCServer(ctx)
	if err != nil {
		return err
	}
	d.server = server

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, daemonSignals...)
	defer signal.Stop(sigChan)

	sig := <-sigChan
	d.log.log("Received signal: %v", sig)
	d.log.log("Shutting down global daemon...")

	cancel()
	if err := d.server.Stop(); err != nil {
		d.log.log("Error stopping server: %v", err)
	}

	d.log.log("Global daemon stopped")
	return nil
}

func getGlobalBeadsDir() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", fmt.Errorf("cannot get home directory: %w", err)
	}

	beadsDir := filepath.Join(home, ".beads")
	if err := os.MkdirAll(beadsDir, 0700); err != nil {
		return "", fmt.Errorf("cannot create global beads directory: %w", err)
	}

	return beadsDir, nil
}



func (d *Daemon) setupLock() (io.Closer, error) {
	beadsDir := filepath.Dir(d.cfg.PIDFile)
	lock, err := acquireDaemonLock(beadsDir, d.cfg.DBPath, d.Version)
	if err != nil {
		if err == ErrDaemonLocked {
			d.log.log("Daemon already running (lock held), exiting")
		} else {
			d.log.log("Error acquiring daemon lock: %v", err)
		}
		return nil, err
	}

	myPID := os.Getpid()
	// #nosec G304 - controlled path from config
	if data, err := os.ReadFile(d.cfg.PIDFile); err == nil {
		if pid, err := strconv.Atoi(strings.TrimSpace(string(data))); err == nil && pid == myPID {
			// PID file is correct, continue
		} else {
			d.log.log("PID file has wrong PID (expected %d, got %d), overwriting", myPID, pid)
			_ = os.WriteFile(d.cfg.PIDFile, []byte(fmt.Sprintf("%d\n", myPID)), 0600)
		}
	} else {
		d.log.log("PID file missing after lock acquisition, creating")
		_ = os.WriteFile(d.cfg.PIDFile, []byte(fmt.Sprintf("%d\n", myPID)), 0600)
	}

	return lock, nil
}

// Stop gracefully shuts down the daemon
func (d *Daemon) Stop() error {
	if d.cancel != nil {
		d.cancel()
	}
	if d.server != nil {
		return d.server.Stop()
	}
	return nil
}

func (d *Daemon) determineDatabasePath() error {
	if d.cfg.DBPath != "" {
		return nil
	}

	// Use public API to find database
	foundDB := beads.FindDatabasePath()
	if foundDB == "" {
		d.log.log("Error: no beads database found")
		d.log.log("Hint: run 'bd init' to create a database or set BEADS_DB environment variable")
		return fmt.Errorf("no beads database found")
	}

	d.cfg.DBPath = foundDB
	d.cfg.BeadsDir = filepath.Dir(foundDB)
	d.cfg.WorkspacePath = filepath.Dir(d.cfg.BeadsDir)
	d.cfg.SocketPath = filepath.Join(d.cfg.BeadsDir, "bd.sock")
	return nil
}

func (d *Daemon) validateSingleDatabase() error {
	// Check for multiple .db files (ambiguity error)
	matches, err := filepath.Glob(filepath.Join(d.cfg.BeadsDir, "*.db"))
	if err == nil && len(matches) > 1 {
		// Filter out backup files
		var validDBs []string
		for _, match := range matches {
			baseName := filepath.Base(match)
			if !strings.Contains(baseName, ".backup") && baseName != "vc.db" {
				validDBs = append(validDBs, match)
			}
		}
		if len(validDBs) > 1 {
			errMsg := fmt.Sprintf("Error: Multiple database files found in %s:\n", d.cfg.BeadsDir)
			for _, db := range validDBs {
				errMsg += fmt.Sprintf("  - %s\n", filepath.Base(db))
			}
			errMsg += fmt.Sprintf("\nBeads requires a single canonical database: %s\n", beads.CanonicalDatabaseName)
			errMsg += "Run 'bd init' to migrate legacy databases or manually remove old databases\n"
			errMsg += "Or run 'bd doctor' for more diagnostics"

			d.log.log(errMsg)

			// Write error to file so user can see it without checking logs
			errFile := filepath.Join(d.cfg.BeadsDir, "daemon-error")
			_ = os.WriteFile(errFile, []byte(errMsg), 0644)

			return fmt.Errorf("multiple database files found")
		}
	}

	// Validate using canonical name
	dbBaseName := filepath.Base(d.cfg.DBPath)
	if dbBaseName != beads.CanonicalDatabaseName {
		d.log.log("Error: Non-canonical database name: %s", dbBaseName)
		d.log.log("Expected: %s", beads.CanonicalDatabaseName)
		d.log.log("")
		d.log.log("Run 'bd init' to migrate to canonical name")
		return fmt.Errorf("non-canonical database name: %s", dbBaseName)
	}

	return nil
}

func (d *Daemon) validateSchemaVersion() error {
	ctx := context.Background()
	dbVersion, err := d.store.GetMetadata(ctx, "bd_version")
	if err != nil && err.Error() != "metadata key not found: bd_version" {
		d.log.log("Error: failed to read database version: %v", err)
		return fmt.Errorf("failed to read database version: %w", err)
	}

	if dbVersion != "" && dbVersion != d.Version {
		d.log.log("Error: Database schema version mismatch")
		d.log.log("  Database version: %s", dbVersion)
		d.log.log("  Daemon version: %s", d.Version)
		d.log.log("")
		d.log.log("The database was created with a different version of bd.")
		d.log.log("This may cause compatibility issues.")
		d.log.log("")
		d.log.log("Options:")
		d.log.log("  1. Run 'bd migrate' to update the database to the current version")
		d.log.log("  2. Upgrade/downgrade bd to match database version: %s", dbVersion)
		d.log.log("  3. Set BEADS_IGNORE_VERSION_MISMATCH=1 to proceed anyway (not recommended)")
		d.log.log("")

		if os.Getenv("BEADS_IGNORE_VERSION_MISMATCH") != "1" {
			return fmt.Errorf("database version mismatch")
		}
		d.log.log("Warning: Proceeding despite version mismatch (BEADS_IGNORE_VERSION_MISMATCH=1)")
	} else if dbVersion == "" {
		// Old database without version metadata - set it now
		d.log.log("Warning: Database missing version metadata, setting to %s", d.Version)
		if err := d.store.SetMetadata(ctx, "bd_version", d.Version); err != nil {
			d.log.log("Error: failed to set database version: %v", err)
			return fmt.Errorf("failed to set database version: %w", err)
		}
	}

	return nil
}

func (d *Daemon) registerDaemon() error {
	registry, err := daemon.NewRegistry()
	if err != nil {
		return err
	}

	entry := daemon.RegistryEntry{
		WorkspacePath: d.cfg.WorkspacePath,
		SocketPath:    d.cfg.SocketPath,
		DatabasePath:  d.cfg.DBPath,
		PID:           os.Getpid(),
		Version:       d.Version,
		StartedAt:     time.Now(),
	}

	if err := registry.Register(entry); err != nil {
		return err
	}

	d.log.log("Registered in global registry")
	return nil
}

func (d *Daemon) unregisterDaemon() {
	registry, err := daemon.NewRegistry()
	if err != nil {
		d.log.log("Warning: failed to create registry for unregister: %v", err)
		return
	}

	if err := registry.Unregister(d.cfg.WorkspacePath, os.Getpid()); err != nil {
		d.log.log("Warning: failed to unregister daemon: %v", err)
	}
}
