// Package protocolgen generates every committed Extension protocol artifact
// from the frozen Go wire registry and its canonical JSON Schema document.
package protocolgen

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"go/format"
	"os"
	"path/filepath"
	"strings"

	"reasonix/internal/extension/protocol"
)

const (
	SchemaArtifactPath   = "internal/extension/protocol/schema.generated.json"
	HashArtifactPath     = "internal/extension/protocol/schema_hash.generated.go"
	MarkdownArtifactPath = "docs/EXTENSION_PROTOCOL.generated.md"
	// SDKTypesArtifactPath is the Go DTO mirror compiled into the stdlib-only
	// extension SDK module.
	SDKTypesArtifactPath = "sdk/go/types_generated.go"
)

// Artifact is one deterministic generated file, relative to the repository
// root. Artifacts are always returned in the stable order declared above.
type Artifact struct {
	Path string
	Data []byte
}

// Generate builds all Extension protocol artifacts without reading the
// committed outputs. The schema hash is calculated from the exact JSON bytes
// returned as the schema artifact.
func Generate() ([]Artifact, error) {
	if err := protocol.ValidateRegistry(); err != nil {
		return nil, fmt.Errorf("validate registry: %w", err)
	}
	document, err := protocol.BuildSchemaDocument()
	if err != nil {
		return nil, fmt.Errorf("build schema: %w", err)
	}
	canonical, err := protocol.CanonicalSchemaBytes()
	if err != nil {
		return nil, fmt.Errorf("canonical schema: %w", err)
	}
	encoded, err := json.Marshal(document)
	if err != nil {
		return nil, fmt.Errorf("marshal schema document: %w", err)
	}
	if !bytes.Equal(canonical, encoded) {
		return nil, fmt.Errorf("canonical schema bytes do not match BuildSchemaDocument")
	}

	digest := sha256.Sum256(canonical)
	schemaHash := "sha256:" + hex.EncodeToString(digest[:])
	hashSource, err := generateSchemaHashGo(schemaHash)
	if err != nil {
		return nil, err
	}
	markdown, err := generateMarkdown(schemaHash)
	if err != nil {
		return nil, fmt.Errorf("generate markdown: %w", err)
	}
	sdkTypes, err := generateSDKTypesGo()
	if err != nil {
		return nil, fmt.Errorf("generate sdk types: %w", err)
	}

	return []Artifact{
		{Path: SchemaArtifactPath, Data: append([]byte(nil), canonical...)},
		{Path: HashArtifactPath, Data: hashSource},
		{Path: MarkdownArtifactPath, Data: markdown},
		{Path: SDKTypesArtifactPath, Data: sdkTypes},
	}, nil
}

func generateSchemaHashGo(schemaHash string) ([]byte, error) {
	source := fmt.Sprintf(`// Code generated by cmd/extension-protocol-gen; DO NOT EDIT.

package protocol

// GeneratedSchemaHash is the SHA-256 of schema.generated.json. Handshake
// comparisons use this constant; protocol tests independently recompute it
// from CanonicalSchemaBytes to reject stale generated artifacts.
const GeneratedSchemaHash = %q
`, schemaHash)
	formatted, err := format.Source([]byte(source))
	if err != nil {
		return nil, fmt.Errorf("format schema hash source: %w", err)
	}
	return formatted, nil
}

// generateMarkdown renders the generated method/event/limits/error index.
// Hand-written prose documentation lives elsewhere; this document is the
// machine-frozen contract summary and always carries the schema hash.
func generateMarkdown(schemaHash string) ([]byte, error) {
	var out strings.Builder
	out.WriteString("<!-- Code generated by cmd/extension-protocol-gen; DO NOT EDIT. -->\n\n")
	out.WriteString("# Reasonix Extension Protocol v2 — Generated Index\n\n")
	fmt.Fprintf(&out, "- Protocol ID: `%s`\n", protocol.ProtocolID)
	fmt.Fprintf(&out, "- Protocol major: `%d`\n", protocol.ProtocolMajor)
	fmt.Fprintf(&out, "- Schema: `%s`\n", SchemaArtifactPath)
	fmt.Fprintf(&out, "- Schema hash: `%s`\n\n", schemaHash)
	out.WriteString("Within major v2 only optional fields, new enum values, and new methods may\n")
	out.WriteString("be added; existing required fields, directions, limits, error reasons, and\n")
	out.WriteString("semantics never change.\n\n")

	out.WriteString("## Methods\n\n")
	out.WriteString("| Method | Direction | Class | Params | Result |\n")
	out.WriteString("| --- | --- | --- | --- | --- |\n")
	for _, spec := range protocol.Registry() {
		result := spec.ResultType.Name()
		if spec.Notification() {
			result = "-"
		}
		fmt.Fprintf(&out, "| `%s` | `%s` | `%s` | `%s` | `%s` |\n",
			spec.Name, spec.Direction, spec.Class, spec.ParamsType.Name(), result)
	}

	events := protocol.InterceptEvents()
	fmt.Fprintf(&out, "\n## Intercept events (%d)\n\n", len(events))
	out.WriteString("`extension/intercept` (blocking) and `extension/event` (observation) share\n")
	out.WriteString("these frozen hook points:\n\n")
	for _, event := range events {
		fmt.Fprintf(&out, "- `%s`\n", event)
	}

	limits := protocol.FrozenLimits()
	out.WriteString("\n## Limits\n\n")
	out.WriteString("| Limit | Value |\n")
	out.WriteString("| --- | --- |\n")
	fmt.Fprintf(&out, "| `frameBytes` | %d |\n", limits.FrameBytes)
	fmt.Fprintf(&out, "| `externalizeFieldBytes` | %d |\n", limits.ExternalizeFieldBytes)
	fmt.Fprintf(&out, "| `contentRefChunkBytes` | %d |\n", limits.ContentRefChunkBytes)
	fmt.Fprintf(&out, "| `contentRefObjectBytes` | %d |\n", limits.ContentRefObjectBytes)

	out.WriteString("\n## Errors\n\n")
	out.WriteString("| Reason | JSON-RPC code | Retryable | Message |\n")
	out.WriteString("| --- | --- | --- | --- |\n")
	for _, contract := range protocol.ErrorContracts() {
		fmt.Fprintf(&out, "| `%s` | %d | %t | %s |\n",
			contract.Reason, contract.JSONRPCCode, contract.Retryable, contract.Message)
	}
	return []byte(out.String()), nil
}

// Write writes a generated artifact set below root.
func Write(root string, artifacts []Artifact) error {
	for _, artifact := range artifacts {
		path := filepath.Join(root, filepath.FromSlash(artifact.Path))
		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
			return fmt.Errorf("create directory for %s: %w", artifact.Path, err)
		}
		if err := os.WriteFile(path, artifact.Data, 0o644); err != nil {
			return fmt.Errorf("write %s: %w", artifact.Path, err)
		}
	}
	return nil
}

// Check compares a generated artifact set byte-for-byte with files below root.
func Check(root string, artifacts []Artifact) error {
	for _, artifact := range artifacts {
		path := filepath.Join(root, filepath.FromSlash(artifact.Path))
		committed, err := os.ReadFile(path)
		if err != nil {
			return fmt.Errorf("read %s: %w", artifact.Path, err)
		}
		if !bytes.Equal(committed, artifact.Data) {
			return fmt.Errorf("generated artifact drift: %s", artifact.Path)
		}
	}
	return nil
}
