package crud

import (
	"context"
	"git.nspix.com/golang/micro/gateway/http"
	"git.nspix.com/golang/rest/orm/query"
	"git.nspix.com/golang/rest/orm/schema"
	"github.com/jinzhu/inflection"
	"github.com/russross/blackfriday"
	"gorm.io/gorm"
	"sort"
	"strings"
	"sync"
)

var (
	Default       = New()
	enable  int32 = 1
)

type (
	EachQueryCallback func(val interface{}, field *schema.Schema, search *query.Query, ctx *http.Context) (err error)
	QueryCallback     func(val interface{}, query *query.Query, ctx *http.Context) (err error)
	SaveCallback      func(val interface{}, ctx *http.Context) (err error)
	DeleteCallback    func(val interface{}, ctx *http.Context) (err error)

	Naming interface {
		DisplayName() string
	}

	CRUD struct {
		db              *gorm.DB
		formatter       *Formatter
		once            sync.Once
		ctx             context.Context
		entityLocker    sync.RWMutex
		entities        map[string]*Entity
		formatLocker    sync.RWMutex
		httpSvr         *http.Server
		formatCallbacks map[string]EachQueryCallback
	}
)

func (crud *CRUD) getCallbackByFormat(format string) EachQueryCallback {
	crud.formatLocker.RLock()
	v, ok := crud.formatCallbacks[format]
	crud.formatLocker.RUnlock()
	if ok {
		return v
	}
	return nil
}

func (crud *CRUD) Register(module string, value interface{}, opts ...Option) {
	o := &Options{ctx: crud.ctx, DB: crud.db}
	for _, f := range opts {
		f(o)
	}
	if crud.httpSvr == nil {
		return
	}
	e := newEntity(crud.ctx, value, o.DB)
	e.instance = crud
	e.Callbacks = o.Callbacks
	e.SetModule(module)
	if o.enable != nil {
		e.enable = o.enable
	} else {
		e.enable = &enable
	}
	if o.Formatter == nil {
		e.formatter = crud.formatter
	} else {
		e.formatter = o.Formatter
	}
	crud.entityLocker.Lock()
	crud.entities[e.stmt.Table+"@"+e.Module] = e
	crud.entityLocker.Unlock()
	e.naming.plural = inflection.Plural(e.stmt.Table)
	e.naming.singular = inflection.Singular(e.stmt.Table)
	if o.Scenarios == nil || len(o.Scenarios) == 0 {
		e.Scenarios = []string{ScenarioList, ScenarioView, ScenarioCreate, ScenarioUpdate, ScenarioDelete, ScenarioExport}
	} else {
		e.Scenarios = o.Scenarios
	}
	if v, ok := value.(Naming); ok {
		e.naming.label = v.DisplayName()
	} else {
		e.naming.label = strings.Title(e.naming.singular)
	}
	e.prefix = o.Prefix
	if e.hasScenarios(ScenarioList) {
		e.Urls[ScenarioList] = o.Prefix + "/" + e.naming.plural
		crud.httpSvr.Handle("GET", e.Urls[ScenarioList], e.actionIndex, o.Middleware...)
	}
	if e.hasScenarios(ScenarioView) {
		e.Urls[ScenarioView] = o.Prefix + "/" + e.naming.singular + "/:id"
		crud.httpSvr.Handle("GET", e.Urls[ScenarioView], e.actionView, o.Middleware...)
	}
	if e.hasScenarios(ScenarioCreate) {
		e.Urls[ScenarioCreate] = o.Prefix + "/" + e.naming.singular
		crud.httpSvr.Handle("POST", e.Urls[ScenarioCreate], e.actionCreate, o.Middleware...)
	}
	if e.hasScenarios(ScenarioUpdate) {
		e.Urls[ScenarioUpdate] = o.Prefix + "/" + e.naming.singular + "/:id"
		crud.httpSvr.Handle("PUT", e.Urls[ScenarioUpdate], e.actionUpdate, o.Middleware...)
	}
	if e.hasScenarios(ScenarioDelete) {
		e.Urls[ScenarioDelete] = o.Prefix + "/" + e.naming.singular + "/:id"
		crud.httpSvr.Handle("DELETE", e.Urls[ScenarioDelete], e.actionDelete, o.Middleware...)
	}
	if e.hasScenarios(ScenarioExport) {
		e.Urls[ScenarioExport] = o.Prefix + "/" + e.naming.singular + "-export"
		crud.httpSvr.Handle("GET", e.Urls[ScenarioExport], e.actionExport, o.Middleware...)
	}
}

func (crud *CRUD) AttachFormatQueryHook(format string, cb EachQueryCallback) {
	crud.formatLocker.Lock()
	crud.formatCallbacks[format] = cb
	crud.formatLocker.Unlock()
}

func (crud *CRUD) SetDB(db *gorm.DB) {
	crud.db = db
}

func (crud *CRUD) GetDB() *gorm.DB {
	return crud.db
}

func (crud *CRUD) SetHttpServer(svr *http.Server) {
	crud.httpSvr = svr
	crud.once.Do(func() {
		crud.bindDocAction()
	})
}

func (crud *CRUD) SetContext(ctx context.Context) {
	crud.ctx = ctx
	if crud.httpSvr == nil {
		crud.httpSvr = http.FromContext(ctx)
	}
	if crud.db == nil {
		crud.db = ctx.Value("db").(*gorm.DB)
	}
	crud.once.Do(func() {
		crud.bindDocAction()
	})
}

func (crud *CRUD) readSnapshot() Entities {
	crud.entityLocker.RLock()
	i := 0
	es := make(Entities, len(crud.entities))
	for _, e := range crud.entities {
		es[i] = e
		i++
	}
	crud.entityLocker.RUnlock()
	sort.Sort(es)
	return es
}

func (crud *CRUD) actionCatalog(c *http.Context) (err error) {
	var sb strings.Builder
	sb.WriteString("# 接口目录")
	sb.WriteByte('\n')
	sb.WriteByte('\n')
	es := crud.readSnapshot()
	for _, e := range es {
		sb.WriteString("* [" + e.naming.label + "接口](/crud/doc/" + e.stmt.Table + "@" + e.Module + ")")
		sb.WriteByte('\n')
	}
	c.Response().Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = c.Response().Write([]byte(`<html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel="stylesheet" href="//rd-api.nspix.com/assets/docs/doc.css"></head><body><div class="markdown-body"><aside id="markdown-toc"></aside><article id="markdown-body">`))
	_, _ = c.Response().Write(blackfriday.Run([]byte(sb.String())))
	_, _ = c.Response().Write([]byte(`</article></div><script src="//rd-api.nspix.com/assets/docs/doc.js"></script></body></html>`))
	return
}

func (crud *CRUD) actionDocs(c *http.Context) (err error) {
	crud.entityLocker.RLock()
	entity, ok := crud.entities[c.ParamValue("id")]
	crud.entityLocker.RUnlock()
	if !ok {
		return c.Error(4004, "record not found")
	}
	c.Response().Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = c.Response().Write([]byte(`<html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel="stylesheet" href="//rd-api.nspix.com/assets/docs/doc.css"><title>` + entity.naming.label + `</title></head><body><div class="markdown-body"><aside id="markdown-toc"></aside><article id="markdown-body">`))
	_, _ = c.Response().Write(blackfriday.Run(crud.generateDoc(entity)))
	_, _ = c.Response().Write([]byte(`</article></div><script src="//rd-api.nspix.com/assets/docs/doc.js"></script></body></html>`))
	return
}

func (crud *CRUD) actionEntities(c *http.Context) (err error) {
	es := crud.readSnapshot()
	names := make([]map[string]interface{}, 0)
	for _, e := range es {
		names = append(names, map[string]interface{}{
			"module":    e.Module,
			"table":     e.stmt.Table,
			"label":     e.naming.label,
			"url":       e.prefix + "/" + e.naming.singular,
			"urls":      e.Urls,
			"scenarios": e.Scenarios,
		})
	}
	return c.Success(names)
}

func (crud *CRUD) bindDocAction() {
	if crud.httpSvr != nil {
		crud.httpSvr.Handle("GET", "/crud/entities", crud.actionEntities)
		crud.httpSvr.Handle("GET", "/crud/docs", crud.actionCatalog)
		crud.httpSvr.Handle("GET", "/crud/doc/:id", crud.actionDocs)
	}
}

func New() *CRUD {
	return &CRUD{
		formatter:       DefaultFormat,
		ctx:             context.Background(),
		formatCallbacks: make(map[string]EachQueryCallback),
		entities:        make(map[string]*Entity),
	}
}
