package main

import (
	"crypto/ed25519"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"
)

// ---------------------------------------------------------------------------
// License config and public status types
// ---------------------------------------------------------------------------

type LicenseConfig struct {
	Enabled           bool   `yaml:"enabled"`
	TrialDays         int    `yaml:"trial_days"`
	RequireAfterTrial bool   `yaml:"require_after_trial"`
	DataDir           string `yaml:"data_dir"`
	PublicKeyBase64   string `yaml:"public_key_base64"`
	ProductCode       string `yaml:"product_code"`
}

const embeddedLicensePublicKeyBase64 = "k0k+ZtOpDWTyO8+uJY9+yL2S/ZzOxyBbaUldw1SJDGc="

var embeddedLicensePolicy = LicenseConfig{
	Enabled:           true,
	TrialDays:         7,
	RequireAfterTrial: true,
	DataDir:           "license",
	PublicKeyBase64:   embeddedLicensePublicKeyBase64,
	ProductCode:       "force_monitor",
}

func runtimeLicenseConfig() LicenseConfig {
	return embeddedLicensePolicy
}

type ActivationRequest struct {
	App              string   `json:"app"`
	Version          string   `json:"version"`
	GeneratedAt      string   `json:"generated_at"`
	Hostname         string   `json:"hostname"`
	Platform         string   `json:"platform"`
	Fingerprint      string   `json:"fingerprint"`
	FingerprintShort string   `json:"fingerprint_short"`
	Components       []string `json:"components"`
}

type LicenseStatus struct {
	Enabled              bool     `json:"enabled"`
	Mode                 string   `json:"mode"`
	Locked               bool     `json:"locked"`
	Message              string   `json:"message"`
	Fingerprint          string   `json:"fingerprint"`
	FingerprintShort     string   `json:"fingerprint_short"`
	Hostname             string   `json:"hostname"`
	TrialDays            int      `json:"trial_days"`
	DaysRemaining        int      `json:"days_remaining"`
	TrialStartedAt       string   `json:"trial_started_at,omitempty"`
	TrialExpiresAt       string   `json:"trial_expires_at,omitempty"`
	Customer             string   `json:"customer,omitempty"`
	LicenseID            string   `json:"license_id,omitempty"`
	ExpiresAt            string   `json:"expires_at,omitempty"`
	Features             []string `json:"features,omitempty"`
	ActivationConfigured bool     `json:"activation_configured"`
	Tampered             bool     `json:"tampered"`
}

type TrialState struct {
	FirstRunUTC string `json:"first_run_utc"`
	LastSeenUTC string `json:"last_seen_utc"`
	Fingerprint string `json:"fingerprint"`
	Checksum    string `json:"checksum"`
	Tampered    bool   `json:"tampered"`
}

type SignedLicense struct {
	App         string   `json:"app"`
	LicenseID   string   `json:"license_id"`
	Customer    string   `json:"customer"`
	Fingerprint string   `json:"fingerprint"`
	IssuedAt    string   `json:"issued_at"`
	ExpiresAt   string   `json:"expires_at,omitempty"`
	Features    []string `json:"features,omitempty"`
	Signature   string   `json:"signature"`
}

type licensePayload struct {
	App         string   `json:"app"`
	LicenseID   string   `json:"license_id"`
	Customer    string   `json:"customer"`
	Fingerprint string   `json:"fingerprint"`
	IssuedAt    string   `json:"issued_at"`
	ExpiresAt   string   `json:"expires_at,omitempty"`
	Features    []string `json:"features,omitempty"`
}

type LicenseManager struct {
	mu          sync.RWMutex
	cfg         LicenseConfig
	dataDir     string
	trialPath   string
	licensePath string
	publicKey   ed25519.PublicKey
	fingerprint string
	components  []string
	hostname    string
	trial       TrialState
	active      *SignedLicense
	lastStatus  LicenseStatus
}

const trialSalt = "force-monitor-trial-v1"

// ---------------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------------

func NewLicenseManager(cfg LicenseConfig, dataDir string) (*LicenseManager, error) {
	if !cfg.Enabled {
		return &LicenseManager{cfg: cfg, dataDir: dataDir}, nil
	}
	if cfg.TrialDays <= 0 {
		cfg.TrialDays = 7
	}
	if strings.TrimSpace(cfg.ProductCode) == "" {
		cfg.ProductCode = "force_monitor"
	}
	if strings.TrimSpace(dataDir) == "" {
		dataDir = "license"
	}
	if err := os.MkdirAll(dataDir, 0o755); err != nil {
		return nil, fmt.Errorf("create license data dir: %w", err)
	}

	fp, comps, err := buildMachineFingerprint(cfg.ProductCode)
	if err != nil {
		return nil, err
	}
	hostname, _ := os.Hostname()

	m := &LicenseManager{
		cfg:         cfg,
		dataDir:     dataDir,
		trialPath:   filepath.Join(dataDir, "trial_state.json"),
		licensePath: filepath.Join(dataDir, "license.json"),
		fingerprint: fp,
		components:  comps,
		hostname:    hostname,
	}

	if strings.TrimSpace(cfg.PublicKeyBase64) != "" {
		pk, err := base64.StdEncoding.DecodeString(strings.TrimSpace(cfg.PublicKeyBase64))
		if err != nil {
			return nil, fmt.Errorf("decode license public key: %w", err)
		}
		if len(pk) != ed25519.PublicKeySize {
			return nil, fmt.Errorf("invalid license public key size")
		}
		m.publicKey = ed25519.PublicKey(pk)
	}

	if err := m.loadOrCreateTrial(); err != nil {
		return nil, err
	}
	if err := m.loadExistingLicense(); err != nil {
		return nil, err
	}
	m.refreshStatusLocked()
	return m, nil
}

// ---------------------------------------------------------------------------
// Public methods
// ---------------------------------------------------------------------------

func (m *LicenseManager) BuildActivationRequest() ActivationRequest {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return ActivationRequest{
		App:              m.cfg.ProductCode,
		Version:          version,
		GeneratedAt:      time.Now().UTC().Format(time.RFC3339),
		Hostname:         m.hostname,
		Platform:         runtime.GOOS + "/" + runtime.GOARCH,
		Fingerprint:      m.fingerprint,
		FingerprintShort: shortFingerprint(m.fingerprint),
		Components:       append([]string(nil), m.components...),
	}
}

func (m *LicenseManager) Status() LicenseStatus {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.refreshStatusLocked()
	return m.lastStatus
}

func (m *LicenseManager) Touch() error {
	if m == nil || !m.cfg.Enabled {
		return nil
	}
	m.mu.Lock()
	defer m.mu.Unlock()

	now := time.Now().UTC()
	lastSeen, _ := time.Parse(time.RFC3339, m.trial.LastSeenUTC)
	if !lastSeen.IsZero() && now.Add(2*time.Minute).Before(lastSeen) {
		m.trial.Tampered = true
	}
	if !lastSeen.IsZero() && now.Sub(lastSeen) < 15*time.Minute && !m.trial.Tampered {
		m.refreshStatusLocked()
		return nil
	}
	m.trial.LastSeenUTC = now.Format(time.RFC3339)
	m.trial.Fingerprint = m.fingerprint
	m.trial.Checksum = m.signTrialState(m.trial)
	if err := writeJSONFileAtomic(m.trialPath, m.trial); err != nil {
		return err
	}
	m.refreshStatusLocked()
	return nil
}

func (m *LicenseManager) ActivateFromText(text string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if !m.cfg.Enabled {
		return errors.New("licensing disabled")
	}
	if len(m.publicKey) != ed25519.PublicKeySize {
		return errors.New("no license public key configured; set the embedded verifier public key")
	}

	text = strings.TrimSpace(text)
	if text == "" {
		return errors.New("license text is empty")
	}

	var lic SignedLicense
	if err := json.Unmarshal([]byte(text), &lic); err != nil {
		return fmt.Errorf("parse license json: %w", err)
	}
	if err := m.validateLicenseLocked(lic); err != nil {
		return err
	}

	if err := os.MkdirAll(filepath.Dir(m.licensePath), 0o755); err != nil {
		return fmt.Errorf("create license dir: %w", err)
	}
	if err := writeBytesAtomic(m.licensePath, []byte(text)); err != nil {
		return fmt.Errorf("write license: %w", err)
	}
	m.active = &lic
	m.refreshStatusLocked()
	return nil
}

// ---------------------------------------------------------------------------
// Internal load / validate
// ---------------------------------------------------------------------------

func (m *LicenseManager) loadOrCreateTrial() error {
	state, err := readJSONFile[TrialState](m.trialPath)
	if err != nil {
		if !errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("read trial state: %w", err)
		}
		now := time.Now().UTC().Format(time.RFC3339)
		state = TrialState{
			FirstRunUTC: now,
			LastSeenUTC: now,
			Fingerprint: m.fingerprint,
		}
		state.Checksum = m.signTrialState(state)
		if err := writeJSONFileAtomic(m.trialPath, state); err != nil {
			return fmt.Errorf("create trial state: %w", err)
		}
	}

	if state.Checksum != m.signTrialState(state) {
		state.Tampered = true
	}
	if state.Fingerprint != "" && state.Fingerprint != m.fingerprint {
		state.Tampered = true
	}
	m.trial = state
	return nil
}

func (m *LicenseManager) loadExistingLicense() error {
	data, err := os.ReadFile(m.licensePath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			m.active = nil
			return nil
		}
		return fmt.Errorf("read existing license: %w", err)
	}
	var lic SignedLicense
	if err := json.Unmarshal(data, &lic); err != nil {
		return fmt.Errorf("parse existing license: %w", err)
	}
	if err := m.validateLicenseLocked(lic); err != nil {
		m.active = nil
		return nil
	}
	m.active = &lic
	return nil
}

func (m *LicenseManager) validateLicenseLocked(lic SignedLicense) error {
	if strings.TrimSpace(lic.App) == "" {
		return errors.New("license app is empty")
	}
	if lic.App != m.cfg.ProductCode {
		return fmt.Errorf("license app mismatch: got %q want %q", lic.App, m.cfg.ProductCode)
	}
	if strings.TrimSpace(lic.Fingerprint) == "" {
		return errors.New("license fingerprint is empty")
	}
	if !strings.EqualFold(strings.TrimSpace(lic.Fingerprint), strings.TrimSpace(m.fingerprint)) {
		return errors.New("license fingerprint does not match this machine")
	}
	if strings.TrimSpace(lic.LicenseID) == "" {
		return errors.New("license_id is required")
	}
	if strings.TrimSpace(lic.IssuedAt) == "" {
		return errors.New("issued_at is required")
	}
	if _, err := time.Parse(time.RFC3339, lic.IssuedAt); err != nil {
		return fmt.Errorf("invalid issued_at: %w", err)
	}
	if strings.TrimSpace(lic.ExpiresAt) != "" {
		exp, err := time.Parse(time.RFC3339, lic.ExpiresAt)
		if err != nil {
			return fmt.Errorf("invalid expires_at: %w", err)
		}
		if time.Now().UTC().After(exp) {
			return errors.New("license has expired")
		}
	}
	if len(m.publicKey) != ed25519.PublicKeySize {
		return errors.New("no public key configured")
	}
	sig, err := base64.StdEncoding.DecodeString(strings.TrimSpace(lic.Signature))
	if err != nil {
		return fmt.Errorf("decode license signature: %w", err)
	}
	payload := licensePayload{
		App:         lic.App,
		LicenseID:   lic.LicenseID,
		Customer:    lic.Customer,
		Fingerprint: lic.Fingerprint,
		IssuedAt:    lic.IssuedAt,
		ExpiresAt:   lic.ExpiresAt,
		Features:    lic.Features,
	}
	b, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("marshal license payload: %w", err)
	}
	if !ed25519.Verify(m.publicKey, b, sig) {
		return errors.New("invalid license signature")
	}
	return nil
}

func (m *LicenseManager) refreshStatusLocked() {
	status := LicenseStatus{
		Enabled:              m.cfg.Enabled,
		Fingerprint:          m.fingerprint,
		FingerprintShort:     shortFingerprint(m.fingerprint),
		Hostname:             m.hostname,
		TrialDays:            m.cfg.TrialDays,
		ActivationConfigured: len(m.publicKey) == ed25519.PublicKeySize,
		Tampered:             m.trial.Tampered,
	}

	if !m.cfg.Enabled {
		status.Mode = "disabled"
		status.Message = "licensing disabled"
		status.Locked = false
		m.lastStatus = status
		return
	}

	if m.active != nil {
		status.Mode = "licensed"
		status.Locked = false
		status.Message = "license active"
		status.Customer = m.active.Customer
		status.LicenseID = m.active.LicenseID
		status.Features = append([]string(nil), m.active.Features...)
		status.ExpiresAt = m.active.ExpiresAt
		m.lastStatus = status
		return
	}

	start, _ := time.Parse(time.RFC3339, m.trial.FirstRunUTC)
	status.TrialStartedAt = m.trial.FirstRunUTC

	if start.IsZero() || m.trial.Tampered {
		status.Mode = "trial_invalid"
		status.Locked = true
		status.Message = "trial state invalid or tampered"
		m.lastStatus = status
		return
	}

	exp := start.Add(time.Duration(m.cfg.TrialDays) * 24 * time.Hour)
	status.TrialExpiresAt = exp.Format(time.RFC3339)
	days := int(time.Until(exp).Hours() / 24)
	if time.Until(exp) > 0 {
		days++
	}
	if days < 0 {
		days = 0
	}
	status.DaysRemaining = days

	if time.Now().UTC().Before(exp) {
		status.Mode = "trial"
		status.Locked = false
		status.Message = fmt.Sprintf("trial active: %d day(s) remaining", status.DaysRemaining)
		m.lastStatus = status
		return
	}

	if m.cfg.RequireAfterTrial {
		status.Mode = "expired"
		status.Locked = true
		status.Message = "trial expired; activation required"
	} else {
		status.Mode = "grace"
		status.Locked = false
		status.Message = "trial expired, but app allowed to continue"
	}
	m.lastStatus = status
}

// ---------------------------------------------------------------------------
// Fingerprint helpers
// ---------------------------------------------------------------------------

func buildMachineFingerprint(productCode string) (string, []string, error) {
	parts := []string{}

	add := func(label, value string) {
		v := normalizeMachineValue(value)
		if v != "" {
			parts = append(parts, label+"="+v)
		}
	}

	if runtime.GOOS == "windows" {
		add("uuid", runWindowsCommand(`(Get-CimInstance Win32_ComputerSystemProduct).UUID`))
		add("board", runWindowsCommand(`(Get-CimInstance Win32_BaseBoard).SerialNumber`))
		add("machineguid", runWindowsCommand(`(Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Cryptography").MachineGuid`))
		add("bios", runWindowsCommand(`(Get-CimInstance Win32_BIOS).SerialNumber`))
	} else {
		add("machineid", readTextFile("/etc/machine-id"))
		add("product_uuid", readTextFile("/sys/class/dmi/id/product_uuid"))
		add("board_serial", readTextFile("/sys/class/dmi/id/board_serial"))
	}

	add("hostname", hostNameSafe())
	add("mac", firstPhysicalMAC())

	if len(parts) == 0 {
		return "", nil, errors.New("could not derive machine fingerprint")
	}

	raw := strings.Join(parts, "|") + "|app=" + normalizeMachineValue(productCode) + "|salt=force-monitor-fp-v1"
	sum := sha256.Sum256([]byte(raw))
	return strings.ToUpper(hex.EncodeToString(sum[:])), parts, nil
}

func normalizeMachineValue(s string) string {
	s = strings.TrimSpace(strings.ToUpper(s))
	s = strings.ReplaceAll(s, "\x00", "")
	s = strings.ReplaceAll(s, "\r", "")
	s = strings.ReplaceAll(s, "\n", "")
	s = strings.ReplaceAll(s, " ", "")
	switch s {
	case "", "TOBEFILLEDBYO.E.M.", "NONE", "DEFAULTSTRING", "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", "00000000-0000-0000-0000-000000000000":
		return ""
	}
	return s
}

func runWindowsCommand(script string) string {
	cmd := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script)
	out, err := cmd.Output()
	if err == nil {
		return string(out)
	}
	cmd = exec.Command("cmd.exe", "/C", "powershell", "-NoProfile", "-NonInteractive", "-Command", script)
	out, err = cmd.Output()
	if err == nil {
		return string(out)
	}
	return ""
}

func readTextFile(path string) string {
	b, err := os.ReadFile(path)
	if err != nil {
		return ""
	}
	return string(b)
}

func hostNameSafe() string {
	h, _ := os.Hostname()
	return h
}

func firstPhysicalMAC() string {
	ifs, err := net.Interfaces()
	if err != nil {
		return ""
	}
	for _, ifc := range ifs {
		if ifc.Flags&net.FlagLoopback != 0 {
			continue
		}
		if ifc.Flags&net.FlagUp == 0 {
			continue
		}
		if len(ifc.HardwareAddr) == 0 {
			continue
		}
		return ifc.HardwareAddr.String()
	}
	return ""
}

func shortFingerprint(full string) string {
	full = strings.TrimSpace(full)
	if len(full) <= 16 {
		return full
	}
	return full[:8] + "-" + full[8:16]
}

// ---------------------------------------------------------------------------
// Trial integrity helpers
// ---------------------------------------------------------------------------

func (m *LicenseManager) signTrialState(state TrialState) string {
	sum := sha256.Sum256([]byte(
		strings.Join([]string{
			state.FirstRunUTC,
			state.LastSeenUTC,
			state.Fingerprint,
			boolToString(state.Tampered),
			trialSalt,
			m.cfg.ProductCode,
		}, "|"),
	))
	return hex.EncodeToString(sum[:])
}

func boolToString(v bool) string {
	if v {
		return "1"
	}
	return "0"
}

// ---------------------------------------------------------------------------
// File helpers
// ---------------------------------------------------------------------------

func writeJSONFileAtomic(path string, v any) error {
	b, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		return err
	}
	return writeBytesAtomic(path, b)
}

func writeBytesAtomic(path string, b []byte) error {
	tmp := path + ".tmp"
	if err := os.WriteFile(tmp, b, 0o644); err != nil {
		return err
	}
	return os.Rename(tmp, path)
}

func readJSONFile[T any](path string) (T, error) {
	var zero T
	b, err := os.ReadFile(path)
	if err != nil {
		return zero, err
	}
	var out T
	if err := json.Unmarshal(b, &out); err != nil {
		return zero, err
	}
	return out, nil
}

// Optional helper for external tools: create canonical bytes to sign.
// Keep this available so you can reuse the same code in a separate
// private license generator app without changing the verification logic.
func MarshalLicensePayloadForSigning(lic SignedLicense) ([]byte, error) {
	payload := licensePayload{
		App:         lic.App,
		LicenseID:   lic.LicenseID,
		Customer:    lic.Customer,
		Fingerprint: lic.Fingerprint,
		IssuedAt:    lic.IssuedAt,
		ExpiresAt:   lic.ExpiresAt,
		Features:    lic.Features,
	}
	return json.Marshal(payload)
}

// The private signing key should live only in a separate offline signer tool.
// This app intentionally does not include any signing helper.

// Small utility for loading a signed license from a reader if you later want
// to support multipart file upload without changing the validation flow.
func ReadLicenseText(r io.Reader) (string, error) {
	b, err := io.ReadAll(r)
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(string(b)), nil
}
