package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"text/tabwriter"
	"time"

	"fn-registry/registry"
)

const (
	defaultAPIURL = "http://localhost:8420"
	pcIDFile      = ".fn_pc"
)

// syncRequest mirrors the server's SyncRequest.
type syncRequest struct {
	PcID      string               `json:"pc_id"`
	Apps      []registry.App       `json:"apps"`
	Analysis  []registry.Analysis  `json:"analysis"`
	Projects  []registry.Project   `json:"projects"`
	Vaults    []registry.Vault     `json:"vaults"`
	Modules   []registry.Module    `json:"modules"`
	Proposals []registry.Proposal  `json:"proposals"`
	Locations []registry.PcLocation `json:"locations"`
}

// syncResponse mirrors the server's SyncResponse.
type syncResponse struct {
	Apps      []registry.App       `json:"apps"`
	Analysis  []registry.Analysis  `json:"analysis"`
	Projects  []registry.Project   `json:"projects"`
	Vaults    []registry.Vault     `json:"vaults"`
	Modules   []registry.Module    `json:"modules"`
	Proposals []registry.Proposal  `json:"proposals"`
	Locations []registry.PcLocation `json:"locations"`
	Stats     struct {
		Received int `json:"received"`
		Updated  int `json:"updated"`
		Sent     int `json:"sent"`
	} `json:"stats"`
}

func cmdSync(args []string) {
	if len(args) > 0 {
		switch args[0] {
		case "status":
			syncStatus()
			return
		case "locations":
			syncLocations()
			return
		case "push":
			syncPushPull()
			return
		case "help", "-h":
			printSyncUsage()
			return
		}
	}

	// Default: full sync
	syncPushPull()
}

func printSyncUsage() {
	fmt.Println(`fn sync — sincroniza registry.db con el servidor central

Usage:
  fn sync                  Push + pull (sync completo)
  fn sync status           Muestra estado del PC actual
  fn sync locations        Mapa de ubicaciones en todos los PCs

Config:
  ~/.fn_pc                 Alias del PC (una linea, ej: "home-wsl")
  FN_REGISTRY_API          URL del servidor (default: http://localhost:8420)
  REGISTRY_API_TOKEN       Token de autenticacion (opcional)`)
}

func syncPushPull() {
	pcID := readPcID()
	if pcID == "" {
		fmt.Fprintln(os.Stderr, "error: ~/.fn_pc not found. Create it with: echo \"my-pc\" > ~/.fn_pc")
		os.Exit(1)
	}

	db := openDB()
	defer db.Close()

	apiBase, _, _ := parseAPIURL()
	fmt.Printf("syncing as %q against %s...\n", pcID, apiBase)

	// 1. Collect local data
	apps, _ := db.AllApps()
	analysis, _ := db.AllAnalysis()
	projects, _ := db.ListAllProjects()
	vaults, _ := db.AllVaults()
	modules, _ := db.AllModules()
	proposals, _ := db.AllProposals()

	// 2. Scan local directories and build pc_locations
	locations := buildLocations(pcID, apps, analysis, projects, vaults)

	// 3. Send to server
	req := syncRequest{
		PcID:      pcID,
		Apps:      apps,
		Analysis:  analysis,
		Projects:  projects,
		Vaults:    vaults,
		Modules:   modules,
		Proposals: proposals,
		Locations: locations,
	}

	body, err := json.Marshal(req)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	apiBase, basicUser, basicPass := parseAPIURL()
	httpReq, err := http.NewRequest("POST", apiBase+"/api/sync", bytes.NewReader(body))
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
	httpReq.Header.Set("Content-Type", "application/json")

	// BasicAuth from URL (https://user:pass@host)
	if basicUser != "" {
		httpReq.SetBasicAuth(basicUser, basicPass)
	}

	// App-level token
	token := os.Getenv("REGISTRY_API_TOKEN")
	if token != "" {
		httpReq.Header.Set("X-Registry-Token", token)
	}

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(httpReq)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: cannot reach server: %v\n", err)
		os.Exit(1)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		fmt.Fprintf(os.Stderr, "error: server returned %d\n", resp.StatusCode)
		os.Exit(1)
	}

	var syncResp syncResponse
	if err := json.NewDecoder(resp.Body).Decode(&syncResp); err != nil {
		fmt.Fprintf(os.Stderr, "error decoding response: %v\n", err)
		os.Exit(1)
	}

	// 4. Apply server data locally
	imported := applySync(db, syncResp)

	fmt.Printf("done. sent %d items, server updated %d, received %d, imported %d locally\n",
		syncResp.Stats.Received, syncResp.Stats.Updated, syncResp.Stats.Sent, imported)
}

// applySync writes server data into local registry.db (newer wins).
func applySync(db *registry.DB, resp syncResponse) int {
	imported := 0

	for _, a := range resp.Apps {
		existing, err := db.GetApp(a.ID)
		if err != nil || a.UpdatedAt.After(existing.UpdatedAt) {
			db.InsertApp(&a)
			imported++
		}
	}

	for _, a := range resp.Analysis {
		existing, err := db.GetAnalysis(a.ID)
		if err != nil || a.UpdatedAt.After(existing.UpdatedAt) {
			db.InsertAnalysis(&a)
			imported++
		}
	}

	for _, p := range resp.Projects {
		existing, err := db.GetProject(p.ID)
		if err != nil || p.UpdatedAt.After(existing.UpdatedAt) {
			db.InsertProject(&p)
			imported++
		}
	}

	for _, v := range resp.Vaults {
		existing, err := db.GetVault(v.ID)
		if err != nil || v.UpdatedAt.After(existing.UpdatedAt) {
			db.InsertVault(&v)
			imported++
		}
	}

	for _, m := range resp.Modules {
		existing, err := db.GetModule(m.ID)
		if err != nil || m.UpdatedAt.After(existing.UpdatedAt) {
			db.InsertModule(&m)
			imported++
		}
	}

	for _, p := range resp.Proposals {
		existing, err := db.GetProposal(p.ID)
		if err != nil || p.UpdatedAt.After(existing.UpdatedAt) {
			db.InsertProposal(&p)
			imported++
		}
	}

	// Locations: import all (server is authoritative)
	for _, loc := range resp.Locations {
		db.InsertPcLocation(&loc)
		imported++
	}

	return imported
}

// buildLocations scans local filesystem to detect which entities exist on this PC.
func buildLocations(pcID string, apps []registry.App, analysis []registry.Analysis, projects []registry.Project, vaults []registry.Vault) []registry.PcLocation {
	r := root()
	now := time.Now().UTC()
	var locs []registry.PcLocation

	for _, a := range apps {
		dirPath := a.DirPath
		if dirPath == "" {
			continue
		}
		absPath := filepath.Join(r, dirPath)
		status := "active"
		if _, err := os.Stat(absPath); err != nil {
			status = "missing"
		}
		locs = append(locs, registry.PcLocation{
			EntityType: "app",
			EntityID:   a.ID,
			PcID:       pcID,
			DirPath:    absPath,
			Status:     status,
			CreatedAt:  now,
			UpdatedAt:  now,
		})
	}

	for _, a := range analysis {
		dirPath := a.DirPath
		if dirPath == "" {
			continue
		}
		absPath := filepath.Join(r, dirPath)
		status := "active"
		if _, err := os.Stat(absPath); err != nil {
			status = "missing"
		}
		locs = append(locs, registry.PcLocation{
			EntityType: "analysis",
			EntityID:   a.ID,
			PcID:       pcID,
			DirPath:    absPath,
			Status:     status,
			CreatedAt:  now,
			UpdatedAt:  now,
		})
	}

	for _, p := range projects {
		dirPath := p.DirPath
		if dirPath == "" {
			continue
		}
		absPath := filepath.Join(r, dirPath)
		status := "active"
		if _, err := os.Stat(absPath); err != nil {
			status = "missing"
		}
		locs = append(locs, registry.PcLocation{
			EntityType: "project",
			EntityID:   p.ID,
			PcID:       pcID,
			DirPath:    absPath,
			Status:     status,
			CreatedAt:  now,
			UpdatedAt:  now,
		})
	}

	for _, v := range vaults {
		path := v.Path
		if path == "" {
			continue
		}
		status := "active"
		if _, err := os.Stat(path); err != nil {
			status = "missing"
		}
		locs = append(locs, registry.PcLocation{
			EntityType: "vault",
			EntityID:   v.ID,
			PcID:       pcID,
			DirPath:    path,
			Status:     status,
			CreatedAt:  now,
			UpdatedAt:  now,
		})
	}

	return locs
}

func syncStatus() {
	pcID := readPcID()
	if pcID == "" {
		fmt.Println("PC: (not configured — create ~/.fn_pc)")
	} else {
		fmt.Printf("PC: %s\n", pcID)
	}
	base, _, _ := parseAPIURL()
	fmt.Printf("API: %s\n", base)

	db := openDB()
	defer db.Close()

	apps, _ := db.AllApps()
	analysis, _ := db.AllAnalysis()
	projects, _ := db.ListAllProjects()
	vaults, _ := db.AllVaults()
	modules, _ := db.AllModules()
	proposals, _ := db.AllProposals()
	locs, _ := db.ListAllPcLocations()

	fmt.Printf("\nLocal registry:\n")
	fmt.Printf("  apps:       %d\n", len(apps))
	fmt.Printf("  analysis:   %d\n", len(analysis))
	fmt.Printf("  projects:   %d\n", len(projects))
	fmt.Printf("  vaults:     %d\n", len(vaults))
	fmt.Printf("  modules:    %d\n", len(modules))
	fmt.Printf("  proposals:  %d\n", len(proposals))
	fmt.Printf("  locations:  %d\n", len(locs))

	// Count by PC
	pcs := map[string]int{}
	for _, l := range locs {
		pcs[l.PcID]++
	}
	if len(pcs) > 0 {
		fmt.Printf("\nKnown PCs:\n")
		for pc, count := range pcs {
			marker := ""
			if pc == pcID {
				marker = " ← this"
			}
			fmt.Printf("  %-20s %d locations%s\n", pc, count, marker)
		}
	}
}

func syncLocations() {
	db := openDB()
	defer db.Close()

	locs, _ := db.ListAllPcLocations()
	if len(locs) == 0 {
		fmt.Println("no locations registered. run 'fn sync' first.")
		return
	}

	pcID := readPcID()

	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	fmt.Fprintf(w, "PC\tTYPE\tENTITY\tPATH\tSTATUS\n")
	for _, l := range locs {
		marker := ""
		if l.PcID == pcID {
			marker = "*"
		}
		fmt.Fprintf(w, "%s%s\t%s\t%s\t%s\t%s\n", l.PcID, marker, l.EntityType, l.EntityID, l.DirPath, l.Status)
	}
	w.Flush()
}

// --- helpers ---

func readPcID() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	data, err := os.ReadFile(filepath.Join(home, pcIDFile))
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(data))
}

func apiURL() string {
	if u := os.Getenv("FN_REGISTRY_API"); u != "" {
		return strings.TrimRight(u, "/")
	}
	return defaultAPIURL
}

// parseAPIURL extracts base URL and optional basicAuth credentials from FN_REGISTRY_API.
// Supports: https://user:pass@host:port
func parseAPIURL() (base, user, pass string) {
	raw := apiURL()
	u, err := url.Parse(raw)
	if err != nil {
		return raw, "", ""
	}
	if u.User != nil {
		user = u.User.Username()
		pass, _ = u.User.Password()
		u.User = nil
		return strings.TrimRight(u.String(), "/"), user, pass
	}
	return raw, "", ""
}
