// Command register creates a Matrix bot user via the Synapse admin API
// and outputs the access token to store in .env.
//
// Usage:
//
//	MATRIX_ADMIN_TOKEN=syt_... go run ./cmd/register \
//	  --homeserver https://matrix-af2f3d.organic-machine.com \
//	  --username assistant-bot \
//	  --displayname "Assistant Bot" \
//	  --env-var MATRIX_TOKEN_ASSISTANT
package main

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	"github.com/spf13/cobra"
)

func main() {
	var (
		homeserver  string
		username    string
		displayname string
		envVar      string
		password    string
	)

	root := &cobra.Command{
		Use:   "register",
		Short: "Register a Matrix bot user via Synapse admin API",
		Long: `Creates a bot user on your Synapse homeserver and prints its access token.

Requires MATRIX_ADMIN_TOKEN env var with an admin user's access token.

Example:
  MATRIX_ADMIN_TOKEN=syt_... go run ./cmd/register \
    --homeserver https://matrix.example.com \
    --username my-bot \
    --displayname "My Bot" \
    --env-var MATRIX_TOKEN_MY_BOT`,
		RunE: func(cmd *cobra.Command, args []string) error {
			adminToken := os.Getenv("MATRIX_ADMIN_TOKEN")
			if adminToken == "" {
				return fmt.Errorf("MATRIX_ADMIN_TOKEN env var is not set")
			}

			// Strip trailing slash
			homeserver = strings.TrimRight(homeserver, "/")

			// Extract server name from homeserver URL
			serverName := homeserver
			serverName = strings.TrimPrefix(serverName, "https://")
			serverName = strings.TrimPrefix(serverName, "http://")

			userID := fmt.Sprintf("@%s:%s", username, serverName)

			fmt.Printf("→ Registering user %s on %s\n", userID, homeserver)

			// Generate password if not provided
			if password == "" {
				password = generatePassword()
			}

			// Step 1: Create/update user via admin API
			if err := createUser(homeserver, adminToken, userID, displayname, password); err != nil {
				return fmt.Errorf("create user: %w", err)
			}
			fmt.Printf("✓ User %s created/updated\n", userID)

			// Step 2: Login as the bot to get an access token
			token, deviceID, err := loginAs(homeserver, username, password)
			if err != nil {
				return fmt.Errorf("login as bot: %w", err)
			}
			fmt.Printf("✓ Logged in, device ID: %s\n", deviceID)

			// Step 3: Generate pickle key for E2EE crypto store
			pickleKey := generatePickleKey()

			// Derive env var prefix from envVar (e.g. MATRIX_TOKEN_FOO → FOO)
			norm := strings.TrimPrefix(envVar, "MATRIX_TOKEN_")

			// Step 4: Print results — parseable lines for register.sh
			fmt.Println("\n─── Add to your .env ───────────────────────────────")
			fmt.Printf("%s=%s\n", envVar, token)
			fmt.Printf("MATRIX_PASSWORD_%s=%s\n", norm, password)
			fmt.Printf("PICKLE_KEY_%s=%s\n", norm, pickleKey)
			fmt.Println("────────────────────────────────────────────────────")
			fmt.Printf("\nUser ID:   %s\n", userID)
			fmt.Printf("Device ID: %s\n", deviceID)

			return nil
		},
	}

	root.Flags().StringVar(&homeserver, "homeserver", "", "Matrix homeserver URL (required)")
	root.Flags().StringVar(&username, "username", "", "Bot username, without @ or server (required)")
	root.Flags().StringVar(&displayname, "displayname", "", "Bot display name shown in Matrix")
	root.Flags().StringVar(&envVar, "env-var", "MATRIX_TOKEN_BOT", "Name of the env var to output")
	root.Flags().StringVar(&password, "password", "", "Bot password (auto-generated if empty)")
	_ = root.MarkFlagRequired("homeserver")
	_ = root.MarkFlagRequired("username")

	if err := root.Execute(); err != nil {
		os.Exit(1)
	}
}

// createUser calls PUT /_synapse/admin/v2/users/@user:server
func createUser(homeserver, adminToken, userID, displayname, password string) error {
	body := map[string]any{
		"password":    password,
		"admin":       false,
		"deactivated": false,
	}
	if displayname != "" {
		body["displayname"] = displayname
	}

	raw, _ := json.Marshal(body)
	url := fmt.Sprintf("%s/_synapse/admin/v2/users/%s", homeserver, userID)

	req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(raw))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+adminToken)
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return fmt.Errorf("HTTP PUT %s: %w", url, err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return fmt.Errorf("admin API returned %d: %s", resp.StatusCode, respBody)
	}
	return nil
}

// loginAs calls POST /_matrix/client/v3/login with the bot credentials.
func loginAs(homeserver, username, password string) (token, deviceID string, err error) {
	body := map[string]any{
		"type": "m.login.password",
		"identifier": map[string]string{
			"type": "m.id.user",
			"user": username,
		},
		"password": password,
	}
	raw, _ := json.Marshal(body)

	url := homeserver + "/_matrix/client/v3/login"
	resp, err := http.Post(url, "application/json", bytes.NewReader(raw))
	if err != nil {
		return "", "", fmt.Errorf("HTTP POST %s: %w", url, err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		return "", "", fmt.Errorf("login returned %d: %s", resp.StatusCode, respBody)
	}

	var result struct {
		AccessToken string `json:"access_token"`
		DeviceID    string `json:"device_id"`
	}
	if err := json.Unmarshal(respBody, &result); err != nil {
		return "", "", fmt.Errorf("parse login response: %w", err)
	}
	return result.AccessToken, result.DeviceID, nil
}

// generatePassword creates a random-enough password for the bot account.
func generatePassword() string {
	f, err := os.Open("/dev/urandom")
	if err != nil {
		return "agent-bot-default-please-change"
	}
	defer f.Close()
	buf := make([]byte, 24)
	_, _ = io.ReadFull(f, buf)
	return fmt.Sprintf("%x", buf)
}

// generatePickleKey creates a 32-byte hex-encoded key for E2EE crypto store encryption.
func generatePickleKey() string {
	f, err := os.Open("/dev/urandom")
	if err != nil {
		return ""
	}
	defer f.Close()
	buf := make([]byte, 32)
	_, _ = io.ReadFull(f, buf)
	return hex.EncodeToString(buf)
}
