package validation

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"gopkg.in/yaml.v3"
)

// --- Rule 18: Each construct must have template files ---

func checkRule18(relDir, constructDir, constructName string, _ AuditOptions) []Violation {
	// Check for entity schema files.
	entries, err := os.ReadDir(constructDir)
	if err != nil {
		return nil
	}
	hasEntitySchema := false
	for _, e := range entries {
		if strings.HasSuffix(e.Name(), ".yaml") && e.Name() != "api.yml" && !strings.Contains(e.Name(), "_template") {
			hasEntitySchema = true
			break
		}
	}
	if !hasEntitySchema {
		return nil
	}

	templatesDir := filepath.Join(constructDir, "templates")
	var out []Violation

	if info, err := os.Stat(templatesDir); err != nil || !info.IsDir() {
		out = append(out, Violation{File: relDir, Message: fmt.Sprintf(`Construct %q has no templates/ subdirectory.`, constructName), Severity: SeverityBlocking, RuleNumber: 18})
		return out
	}

	templateEntries, err := os.ReadDir(templatesDir)
	if err != nil {
		return nil
	}
	hasTemplate := false
	for _, e := range templateEntries {
		if strings.Contains(e.Name(), "_template") {
			hasTemplate = true
			break
		}
	}
	if !hasTemplate {
		out = append(out, Violation{File: relDir, Message: fmt.Sprintf(`Construct %q has templates/ directory but no template files.`, constructName), Severity: SeverityBlocking, RuleNumber: 18})
	}

	// Check for misplaced templates at root.
	for _, e := range entries {
		if strings.Contains(e.Name(), "_template") {
			out = append(out, Violation{File: relDir,
				Message: fmt.Sprintf(`Construct %q has template file(s) at root level: %s. Move to templates/ subdirectory.`, constructName, e.Name()),
				Severity: SeverityBlocking, RuleNumber: 18})
		}
	}

	return out
}

// --- Rule 22: Helper files must have "not autogenerated" marker ---

func checkRule22(relPath, absPath string, _ AuditOptions) []Violation {
	data, err := os.ReadFile(absPath)
	if err != nil {
		return nil
	}
	lines := strings.SplitN(string(data), "\n", 6)
	header := strings.ToLower(strings.Join(lines, "\n"))
	if !strings.Contains(header, "not autogenerated") && !strings.Contains(header, "not auto-generated") {
		return []Violation{{
			File:       relPath,
			Message:    `Manual helper file is missing "not autogenerated" marker in its header. Add // This is not autogenerated. in the first few lines.`,
			Severity:   SeverityBlocking,
			RuleNumber: 22,
		}}
	}
	return nil
}

// --- Rule 34: Template file values must match schema property types ---

func checkRule34(relDir, constructDir string, _ AuditOptions) []Violation {
	templatesDir := filepath.Join(constructDir, "templates")
	if _, err := os.Stat(templatesDir); err != nil {
		return nil
	}

	entitySchemas, err := os.ReadDir(constructDir)
	if err != nil {
		return nil
	}

	var out []Violation
	for _, e := range entitySchemas {
		if !strings.HasSuffix(e.Name(), ".yaml") || e.Name() == "api.yml" || strings.Contains(e.Name(), "_template") {
			continue
		}
		schemaPath := filepath.Join(constructDir, e.Name())
		schema, err := loadEntitySchema(schemaPath)
		if err != nil || schema == nil || schema.Properties == nil {
			continue
		}

		baseName := strings.TrimSuffix(e.Name(), ".yaml")
		tmplEntries, err := os.ReadDir(templatesDir)
		if err != nil {
			continue
		}
		for _, tmplEntry := range tmplEntries {
			if !strings.Contains(tmplEntry.Name(), baseName+"_template") {
				continue
			}
			tmplPath := filepath.Join(templatesDir, tmplEntry.Name())
			tmplData, err := os.ReadFile(tmplPath)
			if err != nil {
				continue
			}
			var tmpl map[string]any
			if strings.HasSuffix(tmplEntry.Name(), ".json") {
				if err := json.Unmarshal(tmplData, &tmpl); err != nil {
					continue
				}
			} else {
				if err := yaml.Unmarshal(tmplData, &tmpl); err != nil {
					continue
				}
			}
			if tmpl == nil {
				continue
			}

			relTmpl := filepath.ToSlash(filepath.Join(relDir, "templates", tmplEntry.Name()))
			for key, val := range tmpl {
				prop, ok := schema.Properties[key]
				if !ok || prop == nil || prop.Type == "" {
					continue
				}
				if val == nil {
					continue
				}
				_, isArray := val.([]any)
				jsType := "object"
				if isArray {
					jsType = "array"
				} else {
					switch val.(type) {
					case string:
						jsType = "string"
					case float64, int:
						jsType = "number"
					case bool:
						jsType = "boolean"
					}
				}

				if prop.Type == "string" && jsType == "object" {
					out = append(out, Violation{File: relTmpl,
						Message:  fmt.Sprintf(`Template property %q is an object but schema declares type: string. Use an empty string "" as the default.`, key),
						Severity: SeverityBlocking, RuleNumber: 34})
				}
				if prop.Type == "array" && jsType == "object" && !isArray {
					out = append(out, Violation{File: relTmpl,
						Message:  fmt.Sprintf(`Template property %q is an object {} but schema declares type: array. Use an empty array [] as the default.`, key),
						Severity: SeverityBlocking, RuleNumber: 34})
				}
				if (prop.Type == "integer" || prop.Type == "number") && jsType == "string" {
					out = append(out, Violation{File: relTmpl,
						Message:  fmt.Sprintf(`Template property %q is a string but schema declares type: %s. Use 0 as the default.`, key, prop.Type),
						Severity: SeverityBlocking, RuleNumber: 34})
				}
			}
		}
	}
	return out
}

// --- Entity-schema check helpers ---

// entityRawProp returns the raw YAML map for a named property in an entity schema.
func entityRawProp(entity *entitySchema, propName string) map[string]any {
	if entity == nil || entity.raw == nil {
		return nil
	}
	props, ok := entity.raw["properties"].(map[string]any)
	if !ok {
		return nil
	}
	raw, _ := props[propName].(map[string]any)
	return raw
}

// entityRawPropExtraTags returns the x-oapi-codegen-extra-tags map for a property.
func entityRawPropExtraTags(entity *entitySchema, propName string) map[string]any {
	raw := entityRawProp(entity, propName)
	if raw == nil {
		return nil
	}
	tags, _ := raw["x-oapi-codegen-extra-tags"].(map[string]any)
	return tags
}

// entityRawPropTag returns a specific tag value from x-oapi-codegen-extra-tags.
// Options after a comma are stripped (e.g. "name,omitempty" -> "name").
func entityRawPropTag(entity *entitySchema, propName, tagName string) string {
	tags := entityRawPropExtraTags(entity, propName)
	if tags == nil {
		return ""
	}
	val, _ := tags[tagName].(string)
	if idx := strings.Index(val, ","); idx >= 0 {
		val = val[:idx]
	}
	return val
}

// entityRawPropGormColumn returns the gorm column from a property's gorm tag.
func entityRawPropGormColumn(entity *entitySchema, propName string) string {
	gormTag := entityRawPropTag(entity, propName, "gorm")
	for _, part := range strings.Split(gormTag, ";") {
		part = strings.TrimSpace(part)
		if strings.HasPrefix(part, "column:") {
			return strings.TrimPrefix(part, "column:")
		}
	}
	return ""
}

// --- Rule 6 for entity schemas: property name casing ---
//
// See checkRule6ForAPI for the Rule 6 docstring. Behaviour is identical for
// entity-schema YAML files: unconditional camelCase, no DB-mirroring
// exception.
func checkRule6ForEntity(filePath string, entity *entitySchema, opts AuditOptions) []Violation {
	sev := classifyStyleIssue(opts)
	if sev == nil || entity == nil || entity.Properties == nil {
		return nil
	}
	var out []Violation
	propNames := make([]string, 0, len(entity.Properties))
	for name := range entity.Properties {
		propNames = append(propNames, name)
	}
	sort.Strings(propNames)
	for _, propName := range propNames {
		if strings.HasPrefix(propName, "$") {
			continue
		}
		issues := GetCamelCaseIssues(propName)
		if len(issues) > 0 {
			descs := make([]string, len(issues))
			for i, iss := range issues {
				descs[i] = iss.Description
			}
			suggestion := GetCamelCaseSuggestion(propName)
			msg := fmt.Sprintf(`Entity property %q %s.`, propName, strings.Join(descs, "; "))
			if suggestion != "" {
				msg += fmt.Sprintf(` Use: %q.`, suggestion)
			}
			msg += schemaPropertyDBContext(propName)
			msg += ` See docs/casing-rules.md.`
			out = append(out, Violation{File: filePath, Message: msg, Severity: *sev, RuleNumber: 6})
		}
	}
	return out
}

// Rule 32 (entity) is retired under the canonical identifier-naming
// contract. The pre-canonical rule required DB-backed property names to
// match their snake_case `db:` tag exactly; under the new contract the
// wire property name is camelCase and the snake_case DB column name lives
// only in `x-oapi-codegen-extra-tags.db`, so propName != col is the
// expected shape for DB-backed fields. See
// docs/casing-rules.md. The function is retained as a
// retired stub so historical audit-pipeline callers can be resolved at
// compile time; it returns no violations.
func checkRule32ForEntity(_ string, _ *entitySchema, _ AuditOptions) []Violation {
	return nil
}

// --- Rule 35 for entity schemas: x-go-type / x-go-type-import consistency ---

func checkRule35ForEntity(filePath string, entity *entitySchema, _ AuditOptions) []Violation {
	if entity == nil || entity.Properties == nil {
		return nil
	}
	var out []Violation
	for propName := range entity.Properties {
		raw := entityRawProp(entity, propName)
		if raw == nil {
			continue
		}
		goType, _ := raw["x-go-type"].(string)
		goImport, _ := raw["x-go-type-import"].(map[string]any)
		if goType == "" || goImport == nil {
			continue
		}
		ext := map[string]any{
			"x-go-type":        goType,
			"x-go-type-import": goImport,
		}
		out = append(out, checkGoTypeConsistency(filePath, "(entity)", propName, ext)...)
	}
	return out
}

// --- Property constraint advisories for entity schemas (Rules 36-41) ---

func checkEntityPropertyConstraints(filePath string, entity *entitySchema, opts AuditOptions) []Violation {
	if entity == nil || entity.Properties == nil {
		return nil
	}
	var out []Violation
	walkEntityPropertyConstraints(filePath, "entity", entity.Properties, entity.raw, opts, &out)
	return out
}

// walkEntityPropertyConstraints recursively walks entity schema properties,
// including nested objects, items, and allOf/oneOf/anyOf combiners.
func walkEntityPropertyConstraints(filePath, scope string, properties map[string]*propertyDef,
	rawParent map[string]any, opts AuditOptions, out *[]Violation) {

	rawProps, _ := rawParent["properties"].(map[string]any)

	for propName, propDef := range properties {
		if propDef == nil {
			continue
		}
		var raw map[string]any
		if rawProps != nil {
			raw, _ = rawProps[propName].(map[string]any)
		}

		fullScope := scope + "." + propName

		// Rule 37: description.
		rawDesc, _ := rawMapString(raw, "description")
		if propDef.Description == "" && rawDesc == "" {
			*out = append(*out, Violation{File: filePath,
				Message:  fmt.Sprintf(`Entity property %q is missing a description.`, propName),
				Severity: classifyDesignIssue(opts), RuleNumber: 37})
		}

		// Rule 38: string constraints. A `const` value is inherently
		// constrained and does not need additional bounds.
		if propDef.Type == "string" && propDef.Ref == "" && len(propDef.Enum) == 0 && propDef.Const == nil {
			hasConstraint := propDef.MinLength != nil || propDef.MaxLength != nil ||
				propDef.Pattern != "" || propDef.Format != ""
			if !hasConstraint {
				*out = append(*out, Violation{File: filePath,
					Message:  fmt.Sprintf(`Entity string property %q has no validation constraint (minLength, maxLength, pattern, or format).`, propName),
					Severity: classifyDesignIssue(opts), RuleNumber: 38})
			}
		}

		// Rule 39: numeric bounds. A `const` value is inherently bounded.
		if propDef.Type == "integer" || propDef.Type == "number" {
			if propDef.Minimum == nil && propDef.Maximum == nil && len(propDef.Enum) == 0 && propDef.Const == nil {
				*out = append(*out, Violation{File: filePath,
					Message:  fmt.Sprintf(`Entity %s property %q has no bounds (minimum/maximum).`, propDef.Type, propName),
					Severity: classifyDesignIssue(opts), RuleNumber: 39})
			}
		}

		// Rule 40: ID properties need format: uuid.
		if idPropertyRE.MatchString(propName) {
			if propDef.Type != "" && propDef.Type != "string" {
				// Non-string typed IDs skip format: uuid check.
			} else if propDef.Ref != "" || propDef.Format == "uuid" {
				// $ref or explicit format -- OK.
			} else if entityPropHasCompositionRef(propDef) {
				// allOf/anyOf/oneOf $ref -- OK.
			} else {
				xIDFormat, _ := rawMapString(raw, "x-id-format")
				if xIDFormat != "external" {
					*out = append(*out, Violation{File: filePath,
						Message: fmt.Sprintf(`Entity ID property %q should have format: uuid, use a $ref to a UUID schema, or add x-id-format: external.`,
							propName),
						Severity: classifyDesignIssue(opts), RuleNumber: 40})
				}
			}
		}

		// Rule 41: page-size minimum.
		if pageSizeNames[propName] && (propDef.Type == "integer" || propDef.Type == "number") {
			if propDef.Minimum == nil || *propDef.Minimum < 1.0 {
				*out = append(*out, Violation{File: filePath,
					Message:  fmt.Sprintf(`Entity page-size property %q must have minimum: 1.`, propName),
					Severity: classifyDesignIssue(opts), RuleNumber: 41})
			}
		}

		// Recurse into nested properties.
		if propDef.Properties != nil {
			rawNested := raw
			if rawNested == nil {
				rawNested = make(map[string]any)
			}
			walkEntityPropertyConstraints(filePath, fullScope, propDef.Properties, rawNested, opts, out)
		}

		// Recurse into items.
		if propDef.Items != nil && propDef.Items.Properties != nil {
			rawItems, _ := raw["items"].(map[string]any)
			if rawItems == nil {
				rawItems = make(map[string]any)
			}
			walkEntityPropertyConstraints(filePath, fullScope+".items", propDef.Items.Properties, rawItems, opts, out)
		}

		// Recurse into allOf/oneOf/anyOf.
		for _, combo := range []struct {
			name string
			defs []map[string]any
		}{
			{"allOf", propDef.AllOf},
			{"oneOf", propDef.OneOf},
			{"anyOf", propDef.AnyOf},
		} {
			for i, entry := range combo.defs {
				if subProps, ok := entry["properties"].(map[string]any); ok {
					subDefs := parseRawProperties(subProps)
					walkEntityPropertyConstraints(filePath,
						fmt.Sprintf("%s.%s[%d]", fullScope, combo.name, i),
						subDefs, entry, opts, out)
				}
			}
		}
	}
}

// parseRawProperties converts raw YAML property maps to propertyDef structs.
// This populates all constraint fields so the walker applied to nested combiner
// subtrees has the same semantics as the top-level entity property walk.
func parseRawProperties(rawProps map[string]any) map[string]*propertyDef {
	result := make(map[string]*propertyDef)
	for name, val := range rawProps {
		m, ok := val.(map[string]any)
		if !ok {
			continue
		}
		p := &propertyDef{raw: m}
		if t, ok := m["type"].(string); ok {
			p.Type = t
		}
		if r, ok := m["$ref"].(string); ok {
			p.Ref = r
		}
		if d, ok := m["description"].(string); ok {
			p.Description = d
		}
		if f, ok := m["format"].(string); ok {
			p.Format = f
		}
		if pat, ok := m["pattern"].(string); ok {
			p.Pattern = pat
		}
		if enum, ok := m["enum"].([]any); ok {
			p.Enum = enum
		}
		if c, ok := m["const"]; ok {
			p.Const = c
		}
		if ml, ok := rawInt(m, "minLength"); ok {
			p.MinLength = &ml
		}
		if ml, ok := rawInt(m, "maxLength"); ok {
			p.MaxLength = &ml
		}
		if mn, ok := rawFloat(m, "minimum"); ok {
			p.Minimum = &mn
		}
		if mx, ok := rawFloat(m, "maximum"); ok {
			p.Maximum = &mx
		}
		if nestedProps, ok := m["properties"].(map[string]any); ok {
			p.Properties = parseRawProperties(nestedProps)
		}
		if items, ok := m["items"].(map[string]any); ok {
			p.Items = parseRawPropertyDef(items)
		}
		if aa, ok := m["allOf"].([]any); ok {
			p.AllOf = rawMapSlice(aa)
		}
		if ao, ok := m["anyOf"].([]any); ok {
			p.AnyOf = rawMapSlice(ao)
		}
		if oo, ok := m["oneOf"].([]any); ok {
			p.OneOf = rawMapSlice(oo)
		}
		result[name] = p
	}
	return result
}

// parseRawPropertyDef converts a single raw YAML schema map to a propertyDef.
func parseRawPropertyDef(m map[string]any) *propertyDef {
	wrapped := map[string]any{"item": m}
	parsed := parseRawProperties(wrapped)
	return parsed["item"]
}

// rawInt extracts an integer value from a raw YAML map.
func rawInt(m map[string]any, key string) (int, bool) {
	v, ok := m[key]
	if !ok {
		return 0, false
	}
	switch n := v.(type) {
	case int:
		return n, true
	case int64:
		return int(n), true
	case float64:
		return int(n), true
	}
	return 0, false
}

// rawFloat extracts a float value from a raw YAML map.
func rawFloat(m map[string]any, key string) (float64, bool) {
	v, ok := m[key]
	if !ok {
		return 0, false
	}
	switch n := v.(type) {
	case float64:
		return n, true
	case int:
		return float64(n), true
	case int64:
		return float64(n), true
	}
	return 0, false
}

// rawMapSlice converts a []any of raw YAML entries into []map[string]any.
func rawMapSlice(arr []any) []map[string]any {
	out := make([]map[string]any, 0, len(arr))
	for _, item := range arr {
		if m, ok := item.(map[string]any); ok {
			out = append(out, m)
		}
	}
	return out
}

// rawMapString returns a string value from a raw map by key, or ("", false).
func rawMapString(m map[string]any, key string) (string, bool) {
	if m == nil {
		return "", false
	}
	v, ok := m[key].(string)
	return v, ok
}

// entityPropHasCompositionRef returns true if any allOf/anyOf/oneOf entry in a
// propertyDef has a "$ref" key.
func entityPropHasCompositionRef(p *propertyDef) bool {
	if p == nil {
		return false
	}
	for _, combo := range [][]map[string]any{p.AllOf, p.AnyOf, p.OneOf} {
		for _, entry := range combo {
			if _, ok := entry["$ref"]; ok {
				return true
			}
		}
	}
	return false
}
