// Package apispec embeds the OpenAPI document, looks up per-operation
// slices, and writes the locked 501 envelope. The 501 body carries the
// operation's slice of the OpenAPI document so consumers discover the
// contract from the endpoint itself — no duplicate planned/contract
// metadata lives in code.
//
// The same document is served verbatim at /api/v1/openapi.yaml so
// tooling that wants the whole spec can fetch it once.
package apispec

import (
	_ "embed"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"sync"

	"github.com/go-chi/chi/v5/middleware"
	yaml "gopkg.in/yaml.v3"
)

//go:embed openapi.yaml
var openapiYAML []byte

// Spec is the parsed, in-memory view of the embedded OpenAPI document. It
// preserves the YAML shape verbatim so the JSON we emit on 501 responses
// matches the on-disk source.
type Spec struct {
	doc     map[string]any
	rawYAML []byte
}

var (
	defaultOnce sync.Once
	defaultSpec *Spec
	defaultErr  error
)

// Default returns the process-wide spec parsed from the embedded YAML. It
// panics on a malformed embed — that is a build-time bug, not a runtime
// one, so failing fast at first use is the right call.
func Default() *Spec {
	defaultOnce.Do(func() {
		s, err := New(openapiYAML)
		defaultSpec = s
		defaultErr = err
	})
	if defaultErr != nil {
		panic(fmt.Sprintf("apispec: embedded openapi.yaml failed to parse: %v", defaultErr))
	}
	return defaultSpec
}

// New parses the supplied YAML bytes. Exposed so tests can construct an
// independent spec without touching the embedded default.
func New(yamlBytes []byte) (*Spec, error) {
	var doc map[string]any
	if err := yaml.Unmarshal(yamlBytes, &doc); err != nil {
		return nil, fmt.Errorf("parse openapi: %w", err)
	}
	if doc == nil {
		return nil, fmt.Errorf("parse openapi: empty document")
	}
	return &Spec{doc: doc, rawYAML: yamlBytes}, nil
}

// YAML returns the raw YAML bytes this spec was built from.
func (s *Spec) YAML() []byte {
	return s.rawYAML
}

// Operation returns the spec slice for a single (method, path) pair, ready
// to be JSON-serialised. The slice is the OpenAPI Operation object (the
// inner block under e.g. paths./projects.get), with parent path-level
// parameters merged in for completeness.
//
// Returns nil if the path or method is not in the spec; that is treated as
// a developer error (route registered without spec coverage) — callers
// log/fail loudly rather than silently writing a partial 501 body.
func (s *Spec) Operation(method, path string) map[string]any {
	paths, _ := s.doc["paths"].(map[string]any)
	if paths == nil {
		return nil
	}
	pathItem, _ := paths[path].(map[string]any)
	if pathItem == nil {
		return nil
	}
	op, _ := pathItem[strings.ToLower(method)].(map[string]any)
	if op == nil {
		return nil
	}

	// Path-level parameters apply to every method on that path; merge them
	// in so the slice is self-contained.
	out := make(map[string]any, len(op)+1)
	for k, v := range op {
		out[k] = v
	}
	if params, ok := pathItem["parameters"]; ok {
		// Prefer the operation's own parameters when both are present;
		// otherwise inherit from the path level.
		if _, exists := out["parameters"]; !exists {
			out["parameters"] = params
		}
	}
	return out
}

// notImplementedResponse is the wire shape for 501 — APIError envelope
// plus a `spec` field carrying the operation slice. Mirrors the
// NotImplementedResponse schema in openapi.yaml.
type notImplementedResponse struct {
	Error     string         `json:"error"`
	Code      string         `json:"code"`
	Message   string         `json:"message"`
	RequestID string         `json:"requestId,omitempty"`
	Spec      map[string]any `json:"spec"`
}

// NotImplemented writes the locked 501 envelope, embedding the OpenAPI
// Operation slice for the capability that is currently unavailable.
func NotImplemented(w http.ResponseWriter, r *http.Request, method, path string) {
	op := Default().Operation(method, path)
	if op == nil {
		panic(fmt.Sprintf("apispec: missing operation for %s %s", method, path))
	}
	body := notImplementedResponse{
		Error:     "not_implemented",
		Code:      "NOT_IMPLEMENTED",
		Message:   method + " " + path + " is unavailable in this daemon",
		RequestID: middleware.GetReqID(r.Context()),
		Spec:      op,
	}
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(http.StatusNotImplemented)
	// A write error here means the client went away mid-response.
	_ = json.NewEncoder(w).Encode(body)
}

// ServeYAML serves the embedded OpenAPI document for SDK generators, tests, and
// developer tooling.
func ServeYAML(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "application/yaml; charset=utf-8")
	_, _ = w.Write(openapiYAML)
}
