package infra

import (
	"bufio"
	"database/sql"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	_ "github.com/mattn/go-sqlite3"
)

// LocationDrift describes a discrepancy between pc_locations and the real disk state.
type LocationDrift struct {
	EntityType string // app, analysis, project, vault
	EntityID   string // id of the artefact
	DirPath    string // dir_path registered or detected
	Status     string // value in pc_locations (active/missing/archived) or "" if not registered
	Issue      string // "missing_on_disk" | "untracked_on_disk" | "status_should_be_active"
}

// PcLocationsDrift compares pc_locations entries against real disk state for pcID.
// If pcID is empty it is read from the first non-empty line of ~/.fn_pc.
// Returns a slice of drift items (never nil, may be empty).
func PcLocationsDrift(registryRoot string, pcID string) ([]LocationDrift, error) {
	if pcID == "" {
		id, err := readFnPC()
		if err != nil {
			return nil, fmt.Errorf("pc_locations_drift: cannot determine pcID: %w", err)
		}
		pcID = id
	}

	dbPath := filepath.Join(registryRoot, "registry.db")
	db, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=on")
	if err != nil {
		return nil, fmt.Errorf("pc_locations_drift: open registry.db: %w", err)
	}
	defer db.Close()

	// Query A: registered locations for this PC
	rows, err := db.Query(
		`SELECT entity_type, entity_id, dir_path, status FROM pc_locations WHERE pc_id = ?`,
		pcID,
	)
	if err != nil {
		return nil, fmt.Errorf("pc_locations_drift: query pc_locations: %w", err)
	}

	type locRow struct {
		entityType string
		entityID   string
		dirPath    string
		status     string
	}

	var registered []locRow
	registeredKey := map[string]locRow{} // key: entityType+"/"+entityID

	for rows.Next() {
		var r locRow
		if err := rows.Scan(&r.entityType, &r.entityID, &r.dirPath, &r.status); err != nil {
			rows.Close()
			return nil, fmt.Errorf("pc_locations_drift: scan: %w", err)
		}
		registered = append(registered, r)
		registeredKey[r.entityType+"/"+r.entityID] = r
	}
	rows.Close()
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("pc_locations_drift: rows: %w", err)
	}

	drifts := []LocationDrift{}

	// Check registered entries against disk
	for _, r := range registered {
		fullPath := r.dirPath
		if !filepath.IsAbs(fullPath) {
			fullPath = filepath.Join(registryRoot, fullPath)
		}
		exists := dirExists(fullPath)

		if r.status == "active" && !exists {
			drifts = append(drifts, LocationDrift{
				EntityType: r.entityType,
				EntityID:   r.entityID,
				DirPath:    r.dirPath,
				Status:     r.status,
				Issue:      "missing_on_disk",
			})
		} else if r.status == "missing" && exists {
			drifts = append(drifts, LocationDrift{
				EntityType: r.entityType,
				EntityID:   r.entityID,
				DirPath:    r.dirPath,
				Status:     r.status,
				Issue:      "status_should_be_active",
			})
		}
	}

	// Query B: all indexed artefacts (apps + analysis) with dir_path
	type artefact struct {
		entityType string
		id         string
		dirPath    string
	}
	var artefacts []artefact

	for _, q := range []struct {
		table      string
		entityType string
	}{
		{"apps", "app"},
		{"analysis", "analysis"},
	} {
		arows, err := db.Query(fmt.Sprintf(`SELECT id, dir_path FROM %s WHERE dir_path != ''`, q.table))
		if err != nil {
			// Table may not exist in all registry versions; skip gracefully
			continue
		}
		for arows.Next() {
			var a artefact
			a.entityType = q.entityType
			if err := arows.Scan(&a.id, &a.dirPath); err != nil {
				arows.Close()
				return nil, fmt.Errorf("pc_locations_drift: scan %s: %w", q.table, err)
			}
			artefacts = append(artefacts, a)
		}
		arows.Close()
	}

	// Cross: indexed artefact on disk but not in pc_locations for pcID
	for _, a := range artefacts {
		fullPath := a.dirPath
		if !filepath.IsAbs(fullPath) {
			fullPath = filepath.Join(registryRoot, fullPath)
		}
		if !dirExists(fullPath) {
			continue // not on this machine, that's fine
		}
		key := a.entityType + "/" + a.id
		if _, found := registeredKey[key]; !found {
			drifts = append(drifts, LocationDrift{
				EntityType: a.entityType,
				EntityID:   a.id,
				DirPath:    a.dirPath,
				Status:     "",
				Issue:      "untracked_on_disk",
			})
		}
	}

	return drifts, nil
}

// readFnPC reads the first non-empty, non-comment line from ~/.fn_pc.
func readFnPC() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", err
	}
	f, err := os.Open(filepath.Join(home, ".fn_pc"))
	if err != nil {
		return "", fmt.Errorf("~/.fn_pc not found: %w", err)
	}
	defer f.Close()
	sc := bufio.NewScanner(f)
	for sc.Scan() {
		line := strings.TrimSpace(sc.Text())
		if line != "" && !strings.HasPrefix(line, "#") {
			return line, nil
		}
	}
	return "", fmt.Errorf("~/.fn_pc is empty")
}

// dirExists returns true if path is an existing directory.
func dirExists(path string) bool {
	info, err := os.Stat(path)
	return err == nil && info.IsDir()
}
