package hotnews

// Input and output JSON Schema builders for the HotNews catalog. Schemas
// are plain map literals; the output schemas document the passthrough shapes
// the handler fail-closes onto, so optional fields the fixed origin really
// omits or sends as null stay nullable and the envelopes stay open.

const schemaDialect = "https://json-schema.org/draft/2020-12/schema"

// maxSafeInteger mirrors JavaScript's Number.MAX_SAFE_INTEGER bound shared by
// the updatedTime epoch-millisecond counter.
const maxSafeInteger = 9007199254740991

func objectSchema(properties map[string]any, required []string, description string) map[string]any {
	schema := map[string]any{
		"$schema":              schemaDialect,
		"type":                 "object",
		"additionalProperties": false,
		"properties":           properties,
	}
	if description != "" {
		schema["description"] = description
	}
	if len(required) > 0 {
		anyRequired := make([]any, 0, len(required))
		for _, name := range required {
			anyRequired = append(anyRequired, name)
		}
		schema["required"] = anyRequired
	}
	return schema
}

// openObjectSchema is objectSchema with forward-compatible members: the
// passthrough envelopes document the observed shape without forbidding
// fields a later origin version may add.
func openObjectSchema(properties map[string]any, required []string, description string) map[string]any {
	schema := map[string]any{
		"$schema":              schemaDialect,
		"type":                 "object",
		"additionalProperties": true,
		"properties":           properties,
	}
	if description != "" {
		schema["description"] = description
	}
	if len(required) > 0 {
		anyRequired := make([]any, 0, len(required))
		for _, name := range required {
			anyRequired = append(anyRequired, name)
		}
		schema["required"] = anyRequired
	}
	return schema
}

func stringSchema(description string) map[string]any {
	schema := map[string]any{"type": "string"}
	if description != "" {
		schema["description"] = description
	}
	return schema
}

// nullableStringSchema admits the null a passthrough member may carry; the
// field stays optional, so an omitted key also validates.
func nullableStringSchema(description string) map[string]any {
	schema := map[string]any{
		"anyOf": []any{
			map[string]any{"type": "string"},
			map[string]any{"type": "null"},
		},
	}
	describe(schema, description)
	return schema
}

// nullableItemIDSchema mirrors the live newsnow item ID variants.
func nullableItemIDSchema(description string) map[string]any {
	schema := map[string]any{
		"anyOf": []any{
			map[string]any{"type": "string"},
			map[string]any{"type": "integer"},
			map[string]any{"type": "null"},
		},
	}
	describe(schema, description)
	return schema
}

// nullableOpenObjectSchema admits either the per-platform decoration object
// or the explicit null returned by some live feeds.
func nullableOpenObjectSchema(description string) map[string]any {
	schema := map[string]any{
		"anyOf": []any{
			openObjectSchema(nil, nil, ""),
			map[string]any{"type": "null"},
		},
	}
	describe(schema, description)
	return schema
}

func describe(schema map[string]any, description string) {
	if description != "" {
		schema["description"] = description
	}
}

func stringEnumSchema(description string, values []any) map[string]any {
	schema := map[string]any{"type": "string", "enum": values}
	describe(schema, description)
	return schema
}

func arraySchema(items map[string]any, description string) map[string]any {
	schema := map[string]any{"type": "array", "items": items}
	describe(schema, description)
	return schema
}

func epochMillisSchema(description string) map[string]any {
	schema := map[string]any{
		"type":    "integer",
		"minimum": 0,
		"maximum": maxSafeInteger,
	}
	describe(schema, description)
	return schema
}

func getTrendingInputSchema() map[string]any {
	return objectSchema(map[string]any{
		"platform_id": stringEnumSchema(
			"The supported source ID to read; list-platforms serves the closed set.",
			platformIDs()),
	}, []string{"platform_id"}, "Trending feed lookup input.")
}

// trendingItemOutputSchema is the open newsnow item passthrough shape. Item
// members are optional and nullable: the origin omits url/mobileUrl for some
// sources, sends extra as an object whose members vary per platform, and
// TrendRadar's upstream parser tolerates null/empty titles, so nothing on the
// item is required beyond object-ness.
func trendingItemOutputSchema() map[string]any {
	return openObjectSchema(map[string]any{
		"id":        nullableItemIDSchema("The item's stable string or integer identifier."),
		"title":     nullableStringSchema("The trending topic title."),
		"url":       nullableStringSchema("The canonical https link for the topic."),
		"mobileUrl": nullableStringSchema("The mobile https link for the topic."),
		"extra":     nullableOpenObjectSchema("Per-platform decorations, for example a weibo hot flag icon."),
	}, nil, "One trending topic.")
}

func trendingOutputSchema() map[string]any {
	return openObjectSchema(map[string]any{
		"status":      stringEnumSchema("The cache status.", []any{"success", "cache"}),
		"id":          stringEnumSchema("The supported source ID the feed belongs to.", platformIDs()),
		"updatedTime": epochMillisSchema("The origin's cache timestamp in epoch milliseconds."),
		"items":       arraySchema(trendingItemOutputSchema(), "The trending topics in rank order."),
	}, []string{"status", "id", "updatedTime", "items"},
		"The platform's current trending-topic feed envelope.")
}

func listPlatformsOutputSchema() map[string]any {
	entry := openObjectSchema(map[string]any{
		"id":   stringSchema("The supported source ID accepted by get-trending."),
		"name": stringSchema("The platform's display name."),
	}, []string{"id", "name"}, "One supported platform.")
	return objectSchema(map[string]any{
		"platforms": arraySchema(entry, "The closed platform directory in stable catalog order."),
	}, []string{"platforms"}, "The supported trending platforms.")
}
