package web

import (
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"gitea.dooplex.hu/admin/felhom-hub/internal/configgen"
	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)

// R-21 slice C — the operator surface for unclaimed appliances (a box booted from the GENERIC ISO
// that registered itself and is polling for a bind). Lives on the Hosts page: an "Unclaimed
// appliances" section, plus BIND (to a customer) and DISCARD actions.

const applianceStaleAfter = 7 * 24 * time.Hour // no poll in 7 days → badge as stale

// applianceRow is the per-appliance view model.
type applianceRow struct {
	ID              int64
	UUID            string
	MACs            []string
	Product         string
	CPU             string
	MemGB           string
	SSHFingerprints []string
	PairingCode     string // v0.66.0 (R-27): the code the customer reads off the box console + types on /bind
	FirstSeen       *time.Time
	LastSeen        *time.Time
	Stale           bool
	Bound           bool
	BoundCustomer   string
}

// customerPickerOption is one entry in the BIND customer picker. HostCount is DISPLAYED (multi-host
// customers are real — Peti) but never gates the bind.
type customerPickerOption struct {
	CustomerID   string
	CustomerName string
	HostCount    int
}

// sshFingerprint returns the OpenSSH SHA256 fingerprint of one authorized_keys-format line, or "" if
// unparseable. Format: "<type> <base64 blob> [comment]".
func sshFingerprint(line string) string {
	f := strings.Fields(line)
	if len(f) < 2 {
		return ""
	}
	blob, err := base64.StdEncoding.DecodeString(f[1])
	if err != nil {
		return ""
	}
	sum := sha256.Sum256(blob)
	return f[0] + " SHA256:" + base64.RawStdEncoding.EncodeToString(sum[:])
}

// applianceToRow builds the view model (parses hw_summary + computes SSH fingerprints).
func applianceToRow(a store.ApplianceRegistration, now time.Time, customerName func(string) string) applianceRow {
	row := applianceRow{
		ID:          a.ID,
		UUID:        a.UUID,
		PairingCode: configgen.FormatPairingCode(a.PairingCode),
		Bound:       a.Status == store.ApplianceBound,
	}
	if a.MACSet != "" {
		row.MACs = strings.Split(a.MACSet, ",")
	}
	fs := a.FirstSeen
	row.FirstSeen = &fs
	ls := a.LastSeen
	row.LastSeen = &ls
	row.Stale = now.Sub(a.LastSeen) > applianceStaleAfter
	for _, k := range strings.Split(a.SSHHostPubkeys, "\n") {
		if fp := sshFingerprint(k); fp != "" {
			row.SSHFingerprints = append(row.SSHFingerprints, fp)
		}
	}
	if a.HWSummary != "" {
		var hw struct {
			Product string `json:"product"`
			CPU     string `json:"cpu"`
			MemKB   int64  `json:"mem_kb"`
		}
		if json.Unmarshal([]byte(a.HWSummary), &hw) == nil {
			row.Product = hw.Product
			row.CPU = hw.CPU
			if hw.MemKB > 0 {
				row.MemGB = fmt.Sprintf("%.1f GB", float64(hw.MemKB)/1024.0/1024.0)
			}
		}
	}
	if row.Bound {
		row.BoundCustomer = customerName(a.CustomerID)
	}
	return row
}

// gatherUnclaimed builds the Unclaimed-appliances rows + the customer picker (with host counts).
func (s *Server) gatherUnclaimed(now time.Time) ([]applianceRow, []customerPickerOption, error) {
	appls, err := s.store.ListUnclaimedAppliances()
	if err != nil {
		return nil, nil, err
	}
	rows := make([]applianceRow, 0, len(appls))
	for _, a := range appls {
		rows = append(rows, applianceToRow(a, now, s.customerName))
	}
	var picker []customerPickerOption
	if len(rows) > 0 { // only pay for the customer list when there's something to bind
		cfgs, err := s.store.ListCustomerConfigs()
		if err != nil {
			return nil, nil, err
		}
		for _, c := range cfgs {
			hosts, _ := s.store.ListHostsByCustomer(c.CustomerID)
			name := c.CustomerName
			if name == "" {
				name = c.CustomerID
			}
			picker = append(picker, customerPickerOption{CustomerID: c.CustomerID, CustomerName: name, HostCount: len(hosts)})
		}
	}
	return rows, picker, nil
}

// handleApplianceBind — POST /appliances/{id}/bind. Stages the delivery for that appliance's token.
// Does NOT gate on the customer's host count (multi-host customers are real).
func (s *Server) handleApplianceBind(w http.ResponseWriter, r *http.Request, id int64) {
	customerID := strings.TrimSpace(r.FormValue("customer_id"))
	mode := strings.TrimSpace(r.FormValue("mode"))
	if mode == "" {
		mode = "appliance"
	}
	extraArgs := strings.TrimSpace(r.FormValue("extra_args"))
	if customerID == "" {
		http.Error(w, "customer_id is required", http.StatusBadRequest)
		return
	}
	cc, err := s.store.GetCustomerConfig(customerID)
	if err != nil {
		s.logger.Printf("[ERROR] appliance bind %d: customer lookup: %v", id, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}
	if cc == nil {
		http.Error(w, "Unknown customer_id", http.StatusBadRequest)
		return
	}
	if err := s.store.BindAppliance(id, customerID, mode, extraArgs); err != nil {
		s.logger.Printf("[WARN] appliance bind %d → %s refused: %v", id, customerID, err)
		http.Error(w, "Bind failed: "+err.Error(), http.StatusConflict)
		return
	}
	// Provenance is the appliance row (bound_at); audit event now that a customer scopes it.
	if _, err := s.store.SaveEvent(customerID, "appliance_bound", "info",
		"Egy új eszközt (bare-metal telepítés) ehhez az ügyfélhez rendeltünk; a hozzáférést a következő lekérdezéskor megkapja.", "", "hub"); err != nil {
		s.logger.Printf("[WARN] appliance bind %d: save event: %v", id, err)
	}
	s.logger.Printf("[INFO] appliance %d BOUND to customer %s (mode=%s) — delivery staged for its next poll", id, customerID, mode)
	http.Redirect(w, r, "/hosts?flash=appliance_bound", http.StatusSeeOther)
}

// handleApplianceDiscard — POST /appliances/{id}/discard. Ignores the registration + invalidates its
// token. Provenance is the appliance row (discarded_at); no customer to scope an event to.
func (s *Server) handleApplianceDiscard(w http.ResponseWriter, r *http.Request, id int64) {
	if err := s.store.DiscardAppliance(id); err != nil {
		s.logger.Printf("[WARN] appliance discard %d: %v", id, err)
		http.Error(w, "Discard failed: "+err.Error(), http.StatusConflict)
		return
	}
	s.logger.Printf("[INFO] appliance %d DISCARDED (token invalidated; polls now 404)", id)
	http.Redirect(w, r, "/hosts?flash=appliance_discarded", http.StatusSeeOther)
}

// parseApplianceID extracts the {id} from /appliances/{id}/<action>.
func parseApplianceID(path, action string) (int64, bool) {
	rest := strings.TrimPrefix(path, "/appliances/")
	rest = strings.TrimSuffix(rest, "/"+action)
	id, err := strconv.ParseInt(rest, 10, 64)
	if err != nil {
		return 0, false
	}
	return id, true
}
