// KeyLockr legacy direct-client SSO/AppData runnable example.
//
// New mobile integrations should use delegated.go so the App and its backend do
// not call KeyLockr directly. This command remains an explicit compatibility path.
//
// Pure SSO:
//
//	go run . -app-tag YOUR_APP_TAG
//
// SSO + AppData:
//
//	go run . -app-tag YOUR_APP_TAG -app-data
//
// Add -write "hello" to demonstrate a version-CAS AppData update.
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net/url"
	"os"
	"strings"
	"time"
)

func main() {
	defaultName, _ := os.Hostname()
	appTag := flag.String("app-tag", os.Getenv("KEYLOCKR_APP_TAG"), "effective MyDeveloper app_tag")
	deviceName := flag.String("name", defaultName, "physical client/device name")
	requestAppData := flag.Bool("app-data", false, "request encrypted AppData")
	requestAP := flag.Bool("ap", false, "request approved Access Point capability")
	scopes := flag.String("scopes", "", "comma-separated AP scopes; empty requests full AP")
	returnURL := flag.String("return-url", "", "absolute native callback URI; never put it in a QR")
	writeVal := flag.String("write", "", "demo AppData value to write with version CAS")
	flag.Parse()

	if strings.TrimSpace(*appTag) == "" {
		log.Fatal("-app-tag or KEYLOCKR_APP_TAG is required")
	}
	if (*requestAppData || *requestAP) && strings.TrimSpace(*deviceName) == "" {
		log.Fatal("-name is required for AppData or AP")
	}
	if *writeVal != "" && !*requestAppData {
		log.Fatal("-write requires -app-data")
	}

	options := HandshakeOptions{
		AppTag:      strings.TrimSpace(*appTag),
		Name:        strings.TrimSpace(*deviceName),
		AppData:     *requestAppData,
		AccessPoint: *requestAP,
	}
	if *requestAP && strings.TrimSpace(*scopes) != "" {
		options.RequestedScopes = splitScopes(*scopes)
	}

	client := NewClient()
	log.Println("1) Fetching KeyLockr server public keys")
	if err := client.FetchServerKeys(); err != nil {
		log.Fatalf("fetch keys: %v", err)
	}

	log.Printf("2) Starting handshake for capabilities %v", options.Capabilities())
	tmpID, err := client.Handshake(options)
	if err != nil {
		log.Fatalf("handshake: %v", err)
	}

	authURI, err := buildAuthorizationURI(tmpID, *returnURL)
	if err != nil {
		log.Fatalf("return URL: %v", err)
	}
	log.Println("3) Connecting the temporary WebSocket")
	result, err := client.WaitForAuth(tmpID, options, 2*time.Minute, func() {
		if *returnURL == "" {
			log.Println("4) Render this value as one QR code:")
		} else {
			log.Println("4) Open this deep link directly; do not render it as a QR:")
		}
		log.Printf("   %s", authURI)
	})
	if err != nil {
		log.Fatalf("authorization: %v", err)
	}
	log.Printf(
		"5) Authorized app_id=%s safe_id=%s capabilities=%v",
		result.AppID,
		result.SafeID,
		result.Capabilities,
	)

	if options.AccessPoint {
		accountKey, err := client.OpenAccountKey(result.AccountKeyForAP, result.UserEncPk)
		if err != nil {
			log.Fatalf("open AP Account Key: %v", err)
		}
		log.Printf("   AP Account Key opened and validated (%d bytes; value not logged)", len(accountKey))
	}

	if options.AppData {
		runAppData(client, result, *writeVal)
	}

	log.Println("6) Calling /app_verify from this process")
	verify, err := AppVerify(options.AppTag, result.AppID, result.SafeID, client.SignPkB64())
	if err != nil {
		log.Printf("   Verification unavailable: %v", err)
		log.Println("   Configure this machine's fixed backend IP in MyDeveloper server_ips to enable it.")
	} else {
		log.Printf("   valid=%v nickname=%q", verify.Valid, verify.Nickname)
	}
	log.Println("Complete. Use safe_id as the user ID in your own application session.")
}

func runAppData(client *Client, auth *AuthResult, writeVal string) {
	var fileKey *FileKeyData
	version := auth.Version
	encrypted := auth.DataEncrypted
	if len(auth.DataFileKey) > 0 {
		var err error
		fileKey, err = client.OpenDataFileKey(auth.DataFileKey)
		if err != nil {
			log.Fatalf("open authorization AppData filekey: %v", err)
		}
		if auth.DataDeferred {
			log.Println("   Authorization content was deferred; reading AppData without another unlock")
			record, err := client.GetAppData(auth.AppID)
			if err != nil {
				log.Fatalf("read deferred AppData: %v", err)
			}
			version = record.Version
			encrypted = record.DataEncrypted
		} else {
			log.Println("   Using AppData returned in the authorization result")
		}
	} else {
		log.Println("   Legacy Safe result has no filekey; requesting a follow-up unlock")
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
		defer cancel()
		var err error
		fileKey, _, err = client.RequestFileKey(ctx, auth.AppID, 2*time.Second)
		if err != nil {
			log.Fatalf("request AppData filekey: %v", err)
		}
		record, err := client.GetAppData(auth.AppID)
		if err != nil {
			log.Fatalf("read AppData: %v", err)
		}
		version = record.Version
		encrypted = record.DataEncrypted
	}
	if version == "" {
		log.Fatal("AppData authorization result is missing ver")
	}
	if len(encrypted) == 0 {
		log.Printf("   AppData is empty at version %s", version)
	} else {
		plaintext, err := DecryptAppData(encrypted, fileKey)
		if err != nil {
			log.Fatalf("decrypt AppData: %v", err)
		}
		log.Printf(
			"   Decrypted and authenticated %d AppData bytes at version %s; value not logged",
			len(plaintext),
			version,
		)
	}

	if writeVal == "" {
		return
	}
	plaintext := pack(map[string]any{
		"message":    writeVal,
		"updated_at": time.Now().UTC().Format(time.RFC3339),
	})
	newVer, err := client.SetAppData(auth.AppID, version, plaintext, fileKey)
	if err != nil {
		log.Fatalf("write AppData: %v", err)
	}
	log.Printf("   AppData CAS write succeeded; new version is %s", newVer)
}

func splitScopes(value string) []string {
	parts := strings.Split(value, ",")
	scopes := make([]string, 0, len(parts))
	for _, part := range parts {
		scope := strings.TrimSpace(part)
		if scope == "" {
			log.Fatal("-scopes contains an empty value")
		}
		scopes = append(scopes, scope)
	}
	return scopes
}

func buildAuthorizationURI(tmpID, callback string) (string, error) {
	query := url.Values{"tmp_id": []string{tmpID}}
	if callback != "" {
		parsed, err := url.Parse(callback)
		if err != nil || !parsed.IsAbs() {
			return "", fmt.Errorf("return_url must be an absolute URI")
		}
		// BEGIN GENERATED SSO RETURN URL POLICY
		// return-url-blocked: about,blob,content,data,facetime,facetime-audio,file,intent,itms*,javascript,keylockr,mailto,market,sms,sso,tel,telprompt
		blocked := map[string]bool{
			"about":          true,
			"blob":           true,
			"content":        true,
			"data":           true,
			"facetime":       true,
			"facetime-audio": true,
			"file":           true,
			"intent":         true,
			"javascript":     true,
			"keylockr":       true,
			"mailto":         true,
			"market":         true,
			"sms":            true,
			"sso":            true,
			"tel":            true,
			"telprompt":      true,
		}
		scheme := strings.ToLower(parsed.Scheme)
		if blocked[scheme] || strings.HasPrefix(scheme, "itms") {
			return "", fmt.Errorf("return_url scheme %q is blocked", parsed.Scheme)
		}
		// END GENERATED SSO RETURN URL POLICY
		query.Set("return_url", callback)
	}
	return "keylockr://sso?" + query.Encode(), nil
}
