package blueprint

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"regexp"
	"strings"
)

var identRe = regexp.MustCompile(`^[a-z][a-z0-9_]{1,47}$`)
var nonIdentRe = regexp.MustCompile(`[^a-z0-9_]+`)
var multiUnderRe = regexp.MustCompile(`_+`)

var reserved = map[string]struct{}{
	"select": {}, "insert": {}, "update": {}, "delete": {}, "drop": {},
	"create": {}, "alter": {}, "table": {}, "schema": {}, "user": {},
	"where": {}, "from": {}, "join": {}, "grant": {}, "revoke": {},
}

type Blueprint struct {
	Version  string   `json:"version"`
	Meta     Meta     `json:"meta"`
	Storage  Storage  `json:"storage"`
	Entities []Entity `json:"entities"`
	Apis     Apis     `json:"apis"`
	Pages    []Page   `json:"pages"`
	Security Security `json:"security"`
	Seed     *Seed    `json:"seed,omitempty"`
}

type Meta struct {
	Name           string          `json:"name"`
	Slug           string          `json:"slug"`
	Description    string          `json:"description,omitempty"`
	Locale         string          `json:"locale"`
	Confidence     float64         `json:"confidence,omitempty"`
	UIPreset       string          `json:"ui_preset,omitempty"`
	PlatformTitle  string          `json:"platform_title,omitempty"`
	ProjectContext string          `json:"project_context,omitempty"`
	UI             json.RawMessage `json:"ui,omitempty"`
	Source         json.RawMessage `json:"source,omitempty"`
}

type Storage struct {
	Mode       string `json:"mode"`
	Engine     string `json:"engine"`
	SchemaName string `json:"schema_name,omitempty"`
}

type Entity struct {
	Name       string  `json:"name"`
	Table      string  `json:"table"`
	Label      string  `json:"label"`
	PrimaryKey string  `json:"primary_key"`
	Fields     []Field `json:"fields"`
	Indexes    []Index `json:"indexes,omitempty"`
}

type Field struct {
	Name       string          `json:"name"`
	Label      string          `json:"label"`
	Type       string          `json:"type"`
	Nullable   *bool           `json:"nullable,omitempty"`
	Unique     bool            `json:"unique,omitempty"`
	Default    any             `json:"default,omitempty"`
	MaxLength  int             `json:"max_length,omitempty"`
	Precision  int             `json:"precision,omitempty"`
	Scale      int             `json:"scale,omitempty"`
	EnumValues []string        `json:"enum_values,omitempty"`
	UI         json.RawMessage `json:"ui,omitempty"`
}

type Index struct {
	Name    string   `json:"name"`
	Columns []string `json:"columns"`
	Unique  bool     `json:"unique,omitempty"`
}

type Apis struct {
	BasePath  string        `json:"base_path"`
	Resources []APIResource `json:"resources"`
}

type APIResource struct {
	Entity     string   `json:"entity"`
	Path       string   `json:"path"`
	Operations []string `json:"operations"`
	List       *ListOpt `json:"list,omitempty"`
}

type ListOpt struct {
	DefaultPageSize int      `json:"default_page_size,omitempty"`
	MaxPageSize     int      `json:"max_page_size,omitempty"`
	AllowedFilters  []string `json:"allowed_filters,omitempty"`
	AllowedSorts    []string `json:"allowed_sorts,omitempty"`
}

type Page struct {
	ID     string      `json:"id"`
	Title  string      `json:"title"`
	Route  string      `json:"route"`
	Type   string      `json:"type"`
	Entity string      `json:"entity"`
	Layout *PageLayout `json:"layout,omitempty"`
}

type PageLayout struct {
	Preset       string            `json:"preset,omitempty"`
	Columns      []string          `json:"columns,omitempty"`
	Filters      []string          `json:"filters,omitempty"`
	Actions      []string          `json:"actions,omitempty"`
	ActionLabels map[string]string `json:"action_labels,omitempty"`
	FilterStyle  string            `json:"filter_style,omitempty"`
	FormFields   []string          `json:"form_fields,omitempty"`
	Widgets      []PageWidget      `json:"widgets,omitempty"`
}

type PageWidget struct {
	Type           string   `json:"type"`
	Title          string   `json:"title,omitempty"`
	Metric         string   `json:"metric,omitempty"`
	Metrics        []string `json:"metrics,omitempty"`
	GroupBy        string   `json:"group_by,omitempty"`
	XField         string   `json:"x_field,omitempty"`
	Entity         string   `json:"entity,omitempty"`
	Columns        []string `json:"columns,omitempty"`
	LabelField     string   `json:"label_field,omitempty"`
	ValueField     string   `json:"value_field,omitempty"`
	SecondaryField string   `json:"secondary_field,omitempty"`
	WarnField      string   `json:"warn_field,omitempty"`
	FilterField    string   `json:"filter_field,omitempty"`
	FilterOp       string   `json:"filter_op,omitempty"`
	FilterValue    any      `json:"filter_value,omitempty"`
	YUnit          string   `json:"y_unit,omitempty"`
	Variant        string   `json:"variant,omitempty"`
	CycleDays      int      `json:"cycle_days,omitempty"`
}

type Security struct {
	Visibility  string      `json:"visibility"`
	Roles       []Role      `json:"roles"`
	RowPolicies []RowPolicy `json:"row_policies"`
}

type Role struct {
	Name        string   `json:"name"`
	Permissions []string `json:"permissions"`
}

type RowPolicy struct {
	Entity string `json:"entity"`
	Rule   string `json:"rule"`
}

type Seed struct {
	ImportExcel bool `json:"import_excel,omitempty"`
	MaxRows     int  `json:"max_rows,omitempty"`
}

func Parse(raw json.RawMessage) (*Blueprint, error) {
	var bp Blueprint
	if err := json.Unmarshal(raw, &bp); err != nil {
		return nil, fmt.Errorf("invalid blueprint json: %w", err)
	}
	return &bp, nil
}

func (bp *Blueprint) Validate(pathSlug string) error {
	if bp.Version != "1.0" {
		return fmt.Errorf("unsupported version: %s", bp.Version)
	}
	bp.SanitizeIdentifiers()
	// 连字符等非法字符规范为下划线（schema/表名不能含 -）
	bp.Meta.Slug = NormalizeIdent(bp.Meta.Slug)
	pathSlug = NormalizeIdent(pathSlug)
	if err := checkIdent("meta.slug", bp.Meta.Slug); err != nil {
		return err
	}
	if bp.Meta.Slug != pathSlug {
		return fmt.Errorf("meta.slug(%s) != path slug(%s)", bp.Meta.Slug, pathSlug)
	}
	if bp.Meta.Name == "" {
		return fmt.Errorf("meta.name required")
	}
	if bp.Storage.Mode != "schema_per_app" && bp.Storage.Mode != "database_per_app" {
		return fmt.Errorf("unsupported storage.mode: %s", bp.Storage.Mode)
	}
	if bp.Storage.Engine != "postgres" && bp.Storage.Engine != "mysql" {
		return fmt.Errorf("unsupported storage.engine: %s", bp.Storage.Engine)
	}
	if len(bp.Entities) == 0 {
		return fmt.Errorf("entities required")
	}

	entityNames := map[string]Entity{}
	for i, e := range bp.Entities {
		prefix := fmt.Sprintf("entities[%d]", i)
		if err := checkIdent(prefix+".name", e.Name); err != nil {
			return err
		}
		if err := checkIdent(prefix+".table", e.Table); err != nil {
			return err
		}
		if err := checkIdent(prefix+".primary_key", e.PrimaryKey); err != nil {
			return err
		}
		if len(e.Fields) == 0 {
			return fmt.Errorf("%s.fields required", prefix)
		}
		fields := map[string]struct{}{}
		pkFound := false
		for j, f := range e.Fields {
			fp := fmt.Sprintf("%s.fields[%d]", prefix, j)
			if err := checkIdent(fp+".name", f.Name); err != nil {
				return err
			}
			if !validFieldType(f.Type) {
				return fmt.Errorf("%s.type invalid: %s", fp, f.Type)
			}
			f.Type = NormalizeFieldType(f.Type)
			if _, ok := fields[f.Name]; ok {
				return fmt.Errorf("%s duplicate field %s", prefix, f.Name)
			}
			fields[f.Name] = struct{}{}
			if f.Name == e.PrimaryKey {
				pkFound = true
			}
		}
		if !pkFound {
			return fmt.Errorf("%s primary_key %s not in fields", prefix, e.PrimaryKey)
		}
		for j, idx := range e.Indexes {
			ip := fmt.Sprintf("%s.indexes[%d]", prefix, j)
			if err := checkIdent(ip+".name", idx.Name); err != nil {
				return err
			}
			if !strings.HasPrefix(idx.Name, "idx_") {
				return fmt.Errorf("%s.name must start with idx_", ip)
			}
			for _, col := range idx.Columns {
				if _, ok := fields[col]; !ok {
					return fmt.Errorf("%s unknown column %s", ip, col)
				}
			}
		}
		entityNames[e.Name] = e
	}

	if len(bp.Apis.Resources) == 0 {
		return fmt.Errorf("apis.resources required")
	}
	for i, r := range bp.Apis.Resources {
		prefix := fmt.Sprintf("apis.resources[%d]", i)
		if _, ok := entityNames[r.Entity]; !ok {
			return fmt.Errorf("%s unknown entity %s", prefix, r.Entity)
		}
		path := strings.TrimPrefix(r.Path, "/")
		if err := checkIdent(prefix+".path", path); err != nil {
			return err
		}
		if len(r.Operations) == 0 {
			return fmt.Errorf("%s.operations required", prefix)
		}
	}
	return nil
}

// AssignSchemaName 由平台重写 schema；database_per_app 时使用 public。
func (bp *Blueprint) AssignSchemaName(tenantID int64) string {
	if bp.Storage.Mode == "database_per_app" {
		bp.Storage.SchemaName = "public"
		return "public"
	}
	name := scopedIdent(fmt.Sprintf("app_t%d_%s", tenantID, bp.Meta.Slug))
	bp.Storage.SchemaName = name
	return name
}

// AssignDatabaseName database_per_app 时返回独立库名，否则空。
func (bp *Blueprint) AssignDatabaseName(tenantID int64) string {
	if bp.Storage.Mode != "database_per_app" {
		return ""
	}
	return scopedIdent(fmt.Sprintf("appdb_t%d_%s", tenantID, bp.Meta.Slug))
}

// scopedIdent 保留可读前缀，并为超长标识符附加内容哈希，避免不同 slug 截断后碰撞。
func scopedIdent(raw string) string {
	if len(raw) <= 48 {
		return raw
	}
	sum := sha256.Sum256([]byte(raw))
	suffix := fmt.Sprintf("_%x", sum[:6])
	return raw[:48-len(suffix)] + suffix
}

// Revision 返回蓝图内容的稳定短哈希，用于发布回执、缓存隔离与诊断日志。
func (bp *Blueprint) Revision() string {
	if bp == nil {
		return ""
	}
	raw, err := json.Marshal(bp)
	if err != nil {
		return ""
	}
	sum := sha256.Sum256(raw)
	return fmt.Sprintf("%x", sum[:8])
}

func checkIdent(field, v string) error {
	if !identRe.MatchString(v) {
		return fmt.Errorf("%s invalid identifier: %s", field, v)
	}
	if _, bad := reserved[v]; bad {
		return fmt.Errorf("%s reserved identifier: %s", field, v)
	}
	return nil
}

// SanitizeIdentifiers 将 camelCase / 点号等统一为 snake_case，并同步页面与 API 引用。
func (bp *Blueprint) SanitizeIdentifiers() {
	entityRename := map[string]string{}
	for i := range bp.Entities {
		e := &bp.Entities[i]
		oldName := e.Name
		e.Name = NormalizeIdent(e.Name)
		e.Table = NormalizeIdent(e.Table)
		e.PrimaryKey = NormalizeIdent(e.PrimaryKey)
		if oldName != "" {
			entityRename[oldName] = e.Name
			entityRename[NormalizeIdent(oldName)] = e.Name
		}
		fieldRename := map[string]string{}
		used := map[string]int{}
		for j := range e.Fields {
			f := &e.Fields[j]
			old := f.Name
			next := NormalizeIdent(old)
			if n, ok := used[next]; ok {
				used[next] = n + 1
				next = fmt.Sprintf("%s_%d", next, n+1)
			} else {
				used[next] = 1
			}
			f.Name = next
			f.Type = NormalizeFieldType(f.Type)
			if old != "" {
				fieldRename[old] = next
			}
		}
		e.PrimaryKey = renameOrSelf(fieldRename, e.PrimaryKey)
		for j := range e.Indexes {
			idx := &e.Indexes[j]
			idx.Name = NormalizeIdent(idx.Name)
			if !strings.HasPrefix(idx.Name, "idx_") {
				idx.Name = "idx_" + idx.Name
			}
			for k, col := range idx.Columns {
				idx.Columns[k] = renameOrSelf(fieldRename, col)
			}
		}
		// pages / apis that reference this entity's fields
		for pi := range bp.Pages {
			p := &bp.Pages[pi]
			entRef := NormalizeIdent(p.Entity)
			if p.Entity == oldName || p.Entity == e.Name || entRef == e.Name || entRef == NormalizeIdent(oldName) {
				p.Entity = e.Name
				if p.Layout != nil {
					p.Layout.Columns = renameSlice(fieldRename, p.Layout.Columns)
					p.Layout.Filters = renameSlice(fieldRename, p.Layout.Filters)
					p.Layout.FormFields = renameSlice(fieldRename, p.Layout.FormFields)
					for wi := range p.Layout.Widgets {
						w := &p.Layout.Widgets[wi]
						w.Metric = renameOrSelf(fieldRename, w.Metric)
						w.GroupBy = renameOrSelf(fieldRename, w.GroupBy)
						w.XField = renameOrSelf(fieldRename, w.XField)
						w.LabelField = renameOrSelf(fieldRename, w.LabelField)
						w.ValueField = renameOrSelf(fieldRename, w.ValueField)
						w.WarnField = renameOrSelf(fieldRename, w.WarnField)
						w.FilterField = renameOrSelf(fieldRename, w.FilterField)
						w.Metrics = renameSlice(fieldRename, w.Metrics)
						w.Columns = renameSlice(fieldRename, w.Columns)
						if w.Entity == oldName || NormalizeIdent(w.Entity) == e.Name {
							w.Entity = e.Name
						}
					}
				}
			}
		}
		for ri := range bp.Apis.Resources {
			r := &bp.Apis.Resources[ri]
			entRef := NormalizeIdent(r.Entity)
			if r.Entity == oldName || r.Entity == e.Name || entRef == e.Name || entRef == NormalizeIdent(oldName) {
				r.Entity = e.Name
				if r.List != nil {
					r.List.AllowedFilters = renameSlice(fieldRename, r.List.AllowedFilters)
					r.List.AllowedSorts = renameSlice(fieldRename, r.List.AllowedSorts)
				}
			}
		}
	}
	for i := range bp.Pages {
		p := &bp.Pages[i]
		p.Entity = renameOrSelf(entityRename, p.Entity)
	}
	for i := range bp.Apis.Resources {
		r := &bp.Apis.Resources[i]
		r.Entity = renameOrSelf(entityRename, r.Entity)
		path := strings.TrimPrefix(r.Path, "/")
		r.Path = "/" + NormalizeIdent(path)
	}
	for i := range bp.Security.RowPolicies {
		rp := &bp.Security.RowPolicies[i]
		rp.Entity = renameOrSelf(entityRename, rp.Entity)
	}
}

func renameOrSelf(m map[string]string, v string) string {
	if v == "" {
		return v
	}
	if n, ok := m[v]; ok {
		return n
	}
	return NormalizeIdent(v)
}

func renameSlice(m map[string]string, in []string) []string {
	if len(in) == 0 {
		return in
	}
	out := make([]string, len(in))
	for i, v := range in {
		out[i] = renameOrSelf(m, v)
	}
	return out
}

// NormalizeSlug 兼容旧名。
func NormalizeSlug(s string) string { return NormalizeIdent(s) }

// NormalizeIdent 驼峰/点号/连字符 → snake_case，符合 ^[a-z][a-z0-9_]{1,47}$
func NormalizeIdent(s string) string {
	s = strings.TrimSpace(s)
	if s == "" {
		return "field"
	}
	var b strings.Builder
	b.Grow(len(s) + 8)
	prevUnder := false
	for i, r := range s {
		switch {
		case r >= 'A' && r <= 'Z':
			if i > 0 && !prevUnder {
				b.WriteByte('_')
			}
			b.WriteByte(byte(r - 'A' + 'a'))
			prevUnder = false
		case r >= 'a' && r <= 'z' || r >= '0' && r <= '9':
			b.WriteByte(byte(r))
			prevUnder = false
		default:
			// '.', '-', 空格等
			if !prevUnder && b.Len() > 0 {
				b.WriteByte('_')
				prevUnder = true
			}
		}
	}
	s = strings.Trim(b.String(), "_")
	s = multiUnderRe.ReplaceAllString(s, "_")
	if s == "" {
		return "field"
	}
	if s[0] < 'a' || s[0] > 'z' {
		s = "f_" + s
	}
	if len(s) > 48 {
		s = strings.TrimRight(s[:48], "_")
	}
	if s == "" {
		return "field"
	}
	return s
}

// NormalizeFieldType 将常见别名收成蓝图允许的 type。
func NormalizeFieldType(t string) string {
	t = strings.ToLower(strings.TrimSpace(t))
	switch t {
	case "float", "float32", "float64", "double", "number", "numeric", "real", "money":
		return "decimal"
	case "integer", "int32", "int64", "long", "serial":
		if t == "int64" || t == "long" || t == "serial" {
			return "bigint"
		}
		return "int"
	case "bool":
		return "boolean"
	case "varchar", "str", "char":
		return "string"
	case "timestamp", "timestamptz", "time":
		return "datetime"
	case "file", "attachment", "blob":
		return "file_ref"
	case "string", "text", "int", "bigint", "decimal", "boolean", "date", "datetime", "enum", "json", "file_ref":
		return t
	default:
		if t == "" {
			return "string"
		}
		return t
	}
}

func validFieldType(t string) bool {
	switch NormalizeFieldType(t) {
	case "string", "text", "int", "bigint", "decimal", "boolean", "date", "datetime", "enum", "json", "file_ref":
		return true
	default:
		return false
	}
}

func BoolOr(p *bool, def bool) bool {
	if p == nil {
		return def
	}
	return *p
}
