// Copyright 2026 Cisco Systems, Inc. and its affiliates
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package gateway

import (
	"crypto/ed25519"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"
)

// DeviceIdentity holds the Ed25519 keypair for gateway device authentication.
type DeviceIdentity struct {
	PrivateKey ed25519.PrivateKey
	PublicKey  ed25519.PublicKey
	DeviceID   string
}

// LoadOrCreateIdentity loads an existing device keypair from disk or generates
// a new one. The keypair is stored as a PEM-encoded Ed25519 private key.
func LoadOrCreateIdentity(keyFile string) (*DeviceIdentity, error) {
	if data, err := os.ReadFile(keyFile); err == nil {
		return parseIdentity(data)
	}

	pub, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		return nil, fmt.Errorf("gateway: generate device key: %w", err)
	}

	if err := os.MkdirAll(filepath.Dir(keyFile), 0o700); err != nil {
		return nil, fmt.Errorf("gateway: create key dir: %w", err)
	}

	block := &pem.Block{
		Type:  "ED25519 PRIVATE KEY",
		Bytes: priv.Seed(),
	}
	pemData := pem.EncodeToMemory(block)
	if err := os.WriteFile(keyFile, pemData, 0o600); err != nil {
		return nil, fmt.Errorf("gateway: write device key: %w", err)
	}

	return &DeviceIdentity{
		PrivateKey: priv,
		PublicKey:  pub,
		DeviceID:   fingerprint(pub),
	}, nil
}

func parseIdentity(data []byte) (*DeviceIdentity, error) {
	block, _ := pem.Decode(data)
	if block == nil {
		return nil, fmt.Errorf("gateway: invalid PEM in device key file")
	}

	seed := block.Bytes
	if len(seed) != ed25519.SeedSize {
		return nil, fmt.Errorf("gateway: invalid seed length %d (expected %d)", len(seed), ed25519.SeedSize)
	}

	priv := ed25519.NewKeyFromSeed(seed)
	pub := priv.Public().(ed25519.PublicKey)

	return &DeviceIdentity{
		PrivateKey: priv,
		PublicKey:  pub,
		DeviceID:   fingerprint(pub),
	}, nil
}

func fingerprint(pub ed25519.PublicKey) string {
	h := sha256.Sum256(pub)
	return hex.EncodeToString(h[:])
}

// ConnectDeviceParams carries the per-connection fields needed to build the
// v3 challenge-response payload that the OpenClaw gateway verifies.
type ConnectDeviceParams struct {
	ClientID     string
	ClientMode   string
	Role         string
	Scopes       []string
	Token        string
	Nonce        string
	Platform     string
	DeviceFamily string
}

// SignChallenge signs the v3 device auth payload and returns a base64url signature.
func (d *DeviceIdentity) SignChallenge(p ConnectDeviceParams, signedAtMs int64) string {
	scopeStr := strings.Join(p.Scopes, ",")
	token := p.Token
	payload := strings.Join([]string{
		"v3",
		d.DeviceID,
		p.ClientID,
		p.ClientMode,
		p.Role,
		scopeStr,
		fmt.Sprintf("%d", signedAtMs),
		token,
		p.Nonce,
		normalizeMetadata(p.Platform),
		normalizeMetadata(p.DeviceFamily),
	}, "|")
	sig := ed25519.Sign(d.PrivateKey, []byte(payload))
	return base64.RawURLEncoding.EncodeToString(sig)
}

// PublicKeyBase64URL returns the base64url-encoded raw public key.
func (d *DeviceIdentity) PublicKeyBase64URL() string {
	return base64.RawURLEncoding.EncodeToString(d.PublicKey)
}

// ConnectDevice builds the device identity block for the connect params.
func (d *DeviceIdentity) ConnectDevice(p ConnectDeviceParams) map[string]interface{} {
	signedAt := time.Now().UnixMilli()
	return map[string]interface{}{
		"id":        d.DeviceID,
		"publicKey": d.PublicKeyBase64URL(),
		"signature": d.SignChallenge(p, signedAt),
		"signedAt":  signedAt,
		"nonce":     p.Nonce,
	}
}

// RepairPairing writes (or overwrites) the sidecar's device entry into
// OpenClaw's devices/paired.json so the gateway can authenticate on the
// next connect attempt.  This is called automatically when a connect
// handshake fails with "token_missing" or "unauthorized", which happens
// when openclaw regenerates its pairing state (e.g. after a restart).
func (d *DeviceIdentity) RepairPairing(sandboxHome string) error {
	devicesDir := filepath.Join(sandboxHome, ".openclaw", "devices")
	pairedPath := filepath.Join(devicesDir, "paired.json")

	paired := make(map[string]interface{})
	if data, err := os.ReadFile(pairedPath); err == nil {
		_ = json.Unmarshal(data, &paired)
	}

	nowMs := time.Now().UnixMilli()
	scopes := []string{
		"operator.read", "operator.write",
		"operator.admin", "operator.approvals",
	}

	existing, _ := paired[d.DeviceID].(map[string]interface{})
	if existing == nil {
		existing = map[string]interface{}{}
	}

	tokens := existing["tokens"]
	if tokens == nil {
		tokens = map[string]interface{}{}
	}
	createdAt := existing["createdAtMs"]
	if createdAt == nil {
		createdAt = nowMs
	}

	// Platform must match what the connect handshake reports
	// (client.go uses runtime.GOOS); otherwise OpenClaw flags this as
	// `metadata-upgrade` ("device identity changed and must be re-approved")
	// on every reconnect and the gateway link gets stuck in NOT_PAIRED.
	// See gateway-rooted regression: hardcoding "linux" here while
	// connecting from darwin produced an infinite RECONNECTING loop
	// even with a valid paired.json entry.
	paired[d.DeviceID] = map[string]interface{}{
		"deviceId":       d.DeviceID,
		"publicKey":      d.PublicKeyBase64URL(),
		"displayName":    "defenseclaw-sidecar",
		"platform":       runtime.GOOS,
		"deviceFamily":   existing["deviceFamily"],
		"clientId":       "gateway-client",
		"clientMode":     "backend",
		"role":           "operator",
		"roles":          []string{"operator"},
		"scopes":         scopes,
		"approvedScopes": scopes,
		"tokens":         tokens,
		"createdAtMs":    createdAt,
		"approvedAtMs":   nowMs,
	}

	if err := os.MkdirAll(devicesDir, 0o755); err != nil {
		return fmt.Errorf("gateway: repair pairing: mkdir: %w", err)
	}

	data, err := json.MarshalIndent(paired, "", "  ")
	if err != nil {
		return fmt.Errorf("gateway: repair pairing: marshal: %w", err)
	}
	data = append(data, '\n')

	if err := os.WriteFile(pairedPath, data, 0o644); err != nil {
		return fmt.Errorf("gateway: repair pairing: write: %w", err)
	}

	fmt.Fprintf(os.Stderr, "[gateway] repaired device pairing in %s\n", pairedPath)
	return nil
}

func normalizeMetadata(s string) string {
	s = strings.TrimSpace(s)
	if s == "" {
		return ""
	}
	return strings.ToLower(s)
}
