package infra

import (
	"crypto/sha256"
	"database/sql"
	_ "embed"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

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

//go:embed migrations/wg_revoked/001_revoked_peers.sql
var wgRevokedMigration string

// WGPeerRevokeAudit contiene el registro inmutable de una revocacion.
type WGPeerRevokeAudit struct {
	DeviceID  string
	PublicKey string
	RevokedAt int64
	RevokedBy string
	Reason    string
	PrevHash  string
	ThisHash  string
}

// WGPeerRevoke revoca permanentemente un peer: lo elimina del config activo,
// lo registra en una audit DB con hash chain SHA256 inviolable y escribe en
// la blacklist persistente /etc/wireguard/wg_revoked.list.
//
// Reglas:
//   - reason no puede estar vacio.
//   - Revocar un peer ya revocado devuelve error "already revoked".
//   - auditDBPath se crea con migracion embebida si no existe.
//   - this_hash = SHA256(prev_hash || device_id || public_key || revoked_at || revoked_by || reason)
func WGPeerRevoke(deviceID, operator, reason string, configPath, auditDBPath string) (WGPeerRevokeAudit, error) {
	audit := WGPeerRevokeAudit{
		DeviceID:  deviceID,
		RevokedBy: operator,
		Reason:    reason,
	}

	if strings.TrimSpace(deviceID) == "" {
		return audit, fmt.Errorf("wg_peer_revoke: deviceID cannot be empty")
	}
	if strings.TrimSpace(operator) == "" {
		return audit, fmt.Errorf("wg_peer_revoke: operator cannot be empty")
	}
	if strings.TrimSpace(reason) == "" {
		return audit, fmt.Errorf("wg_peer_revoke: reason cannot be empty")
	}
	if strings.TrimSpace(configPath) == "" {
		return audit, fmt.Errorf("wg_peer_revoke: configPath cannot be empty")
	}
	if strings.TrimSpace(auditDBPath) == "" {
		return audit, fmt.Errorf("wg_peer_revoke: auditDBPath cannot be empty")
	}

	// 1. Abrir/crear audit DB y aplicar migracion.
	if err := os.MkdirAll(filepath.Dir(auditDBPath), 0700); err != nil {
		return audit, fmt.Errorf("wg_peer_revoke: mkdir audit db dir: %w", err)
	}
	db, err := sql.Open("sqlite3", auditDBPath+"?_journal_mode=WAL&_foreign_keys=on")
	if err != nil {
		return audit, fmt.Errorf("wg_peer_revoke: open audit db: %w", err)
	}
	defer db.Close()

	if _, err := db.Exec(wgRevokedMigration); err != nil {
		return audit, fmt.Errorf("wg_peer_revoke: apply migration: %w", err)
	}

	// 2. Verificar que no este ya revocado ANTES del lookup (el peer puede
	// haber sido eliminado del config por una revocacion previa).
	var count int
	if err := db.QueryRow("SELECT COUNT(*) FROM revoked_peers WHERE device_id = ?", deviceID).Scan(&count); err != nil {
		return audit, fmt.Errorf("wg_peer_revoke: check existing: %w", err)
	}
	if count > 0 {
		return audit, fmt.Errorf("wg_peer_revoke: device %s already revoked", deviceID)
	}

	// 3. Lookup PublicKey (el peer debe estar aun en el config en este punto).
	pubKey, err := wgLookupPeerPublicKey(deviceID, configPath)
	if err != nil {
		return audit, fmt.Errorf("wg_peer_revoke: lookup peer: %w", err)
	}
	audit.PublicKey = pubKey

	// 4. Obtener prev_hash (hash del ultimo registro, o string vacio si genesis).
	var prevHash string
	_ = db.QueryRow("SELECT this_hash FROM revoked_peers ORDER BY revoked_at DESC LIMIT 1").Scan(&prevHash)

	// 5. Calcular this_hash = SHA256(prevHash || deviceID || publicKey || revokedAt || operator || reason).
	revokedAt := time.Now().Unix()
	audit.RevokedAt = revokedAt
	audit.PrevHash = prevHash

	hashInput := fmt.Sprintf("%s|%s|%s|%d|%s|%s", prevHash, deviceID, pubKey, revokedAt, operator, reason)
	thisHash := fmt.Sprintf("%x", sha256.Sum256([]byte(hashInput)))
	audit.ThisHash = thisHash

	// 6. WGPeerRemove (eliminar del config + syncconf).
	removeResult, err := WGPeerRemove(deviceID, configPath)
	if err != nil {
		return audit, fmt.Errorf("wg_peer_revoke: remove peer: %w", err)
	}
	_ = removeResult // status puede ser removed o not-present (ya borrado, igual revocamos)

	// 7. Insertar en audit DB dentro de una transaccion.
	tx, err := db.Begin()
	if err != nil {
		return audit, fmt.Errorf("wg_peer_revoke: begin tx: %w", err)
	}
	defer func() { _ = tx.Rollback() }()

	_, err = tx.Exec(
		`INSERT INTO revoked_peers (device_id, public_key, revoked_at, revoked_by, reason, prev_hash, this_hash)
		 VALUES (?, ?, ?, ?, ?, ?, ?)`,
		deviceID, pubKey, revokedAt, operator, reason, prevHash, thisHash,
	)
	if err != nil {
		return audit, fmt.Errorf("wg_peer_revoke: insert audit record: %w", err)
	}

	if err := tx.Commit(); err != nil {
		return audit, fmt.Errorf("wg_peer_revoke: commit audit record: %w", err)
	}

	// 8. Append a blacklist persistente /etc/wireguard/wg_revoked.list.
	blacklistLine := fmt.Sprintf("%s %d # DeviceID:%s operator:%s reason:%s\n",
		pubKey, revokedAt, deviceID, operator, reason)
	if err := wgAppendBlacklistFn(blacklistLine); err != nil {
		// No revertir — el audit DB ya tiene el registro. Loguear como advertencia.
		return audit, fmt.Errorf("wg_peer_revoke: append blacklist (audit DB committed): %w", err)
	}

	return audit, nil
}

// wgAppendBlacklistFn escribe una linea al final de /etc/wireguard/wg_revoked.list (append-only).
// Variable para permitir override en tests sin requerir permisos de /etc/wireguard/.
var wgAppendBlacklistFn = func(line string) error {
	const blacklistPath = "/etc/wireguard/wg_revoked.list"
	f, err := os.OpenFile(blacklistPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
	if err != nil {
		return fmt.Errorf("open blacklist %s: %w", blacklistPath, err)
	}
	defer f.Close()
	if _, err := f.WriteString(line); err != nil {
		return fmt.Errorf("write blacklist: %w", err)
	}
	return nil
}
