package dbtable2crud

import (
	"log"
	"strings"

	"geeks-accelerator/oss/saas-starter-kit/tools/truss/internal/goparse"
	"github.com/fatih/structtag"
	"github.com/jmoiron/sqlx"
	"github.com/pkg/errors"
)

// modelDef defines info about the struct and associated db table.
type modelDef struct {
	*goparse.GoDocument
	Name          string
	TableName     string
	PrimaryField  string
	PrimaryColumn string
	PrimaryType   string
	Fields        []modelField
	FieldNames    []string
	ColumnNames   []string
}

// modelField defines a struct field and associated db column.
type modelField struct {
	ColumnName   string
	DbColumn     *psqlColumn
	FieldName    string
	FieldType    string
	FieldIsPtr   bool
	Tags         *structtag.Tags
	ApiHide      bool
	ApiRead      bool
	ApiCreate    bool
	ApiUpdate    bool
	DefaultValue string
}

// parseModelFile parses the entire model file and then loads the specified model struct.
func parseModelFile(db *sqlx.DB, log *log.Logger, dbName, dbTable, modelFile, modelName string) (*modelDef, error) {

	// Parse the supplied model file.
	doc, err := goparse.ParseFile(log, modelFile)
	if err != nil {
		return nil, err
	}

	// Init new modelDef.
	model := &modelDef{
		GoDocument: doc,
		Name:       modelName,
		TableName:  dbTable,
	}

	// Append the field the the model def.
	model.Fields, err = parseModelFields(doc, modelName, nil)
	if err != nil {
		return nil, err
	}

	for _, sf := range model.Fields {
		model.FieldNames = append(model.FieldNames, sf.FieldName)
		model.ColumnNames = append(model.ColumnNames, sf.ColumnName)
	}

	// Query the database for a table definition.
	dbCols, err := descTable(db, dbName, dbTable)
	if err != nil {
		return model, err
	}

	// Loop over all the database table columns and append to the associated
	// struct field. Don't force all database table columns to be defined in the
	// in the struct.
	for _, dbCol := range dbCols {
		for idx, sf := range model.Fields {
			if sf.ColumnName != dbCol.Column {
				continue
			}

			if dbCol.IsPrimaryKey {
				model.PrimaryColumn = sf.ColumnName
				model.PrimaryField = sf.FieldName
				model.PrimaryType = sf.FieldType
			}

			if dbCol.DefaultValue != nil {
				sf.DefaultValue = *dbCol.DefaultValue

				if dbCol.IsEnum {
					sf.DefaultValue = strings.Trim(sf.DefaultValue, "'")
					sf.DefaultValue = sf.FieldType + "_" + FormatCamel(sf.DefaultValue)
				} else if strings.HasPrefix(sf.DefaultValue, "'") {
					sf.DefaultValue = strings.Trim(sf.DefaultValue, "'")
					sf.DefaultValue = "\"" + sf.DefaultValue + "\""
				}
			}

			c := dbCol
			sf.DbColumn = &c
			model.Fields[idx] = sf
		}
	}

	// Print out the model for debugging.
	//modelJSON, err := json.MarshalIndent(model, "", "    ")
	//if err != nil {
	//	return model, errors.WithStack(err )
	//}
	//log.Printf(string(modelJSON))

	return model, nil
}

// parseModelFields parses the fields from a struct.
func parseModelFields(doc *goparse.GoDocument, modelName string, baseModel *modelDef) ([]modelField, error) {

	// Ensure the model file has a struct with the model name supplied.
	if !doc.HasType(modelName, goparse.GoObjectType_Struct) {
		err := errors.Errorf("Struct with the name %s does not exist", modelName)
		return nil, err
	}

	// Load the struct from parsed go file.
	docModel := doc.Get(modelName, goparse.GoObjectType_Struct)

	// Loop over all the objects contained between the struct definition start and end.
	// This should be a list of variables defined for model.
	resp := []modelField{}
	for _, l := range docModel.Objects().List() {

		// Skip all lines that are not a var.
		if l.Type != goparse.GoObjectType_Line {
			log.Printf("parseModelFile : Model %s has line that is %s, not type line, skipping - %s\n", modelName, l.Type, l.String())
			continue
		}

		// Extract the var name, type and defined tags from the line.
		sv, err := goparse.ParseStructProp(l)
		if err != nil {
			return nil, err
		}

		// Init new modelField for the struct var.
		sf := modelField{
			FieldName:  sv.Name,
			FieldType:  sv.Type,
			FieldIsPtr: strings.HasPrefix(sv.Type, "*"),
			Tags:       sv.Tags,
		}

		// Extract the column name from the var tags.
		if sf.Tags != nil {
			// First try to get the column name from the db tag.
			dbt, err := sf.Tags.Get("db")
			if err != nil && !strings.Contains(err.Error(), "not exist") {
				err = errors.WithStack(err)
				return nil, err
			} else if dbt != nil {
				sf.ColumnName = dbt.Name
			}

			// Second try to get the column name from the json tag.
			if sf.ColumnName == "" {
				jt, err := sf.Tags.Get("json")
				if err != nil && !strings.Contains(err.Error(), "not exist") {
					err = errors.WithStack(err)
					return nil, err
				} else if jt != nil && jt.Name != "-" {
					sf.ColumnName = jt.Name
				}
			}

			var apiActionsSet bool
			tt, err := sf.Tags.Get("truss")
			if err != nil && !strings.Contains(err.Error(), "not exist") {
				err = errors.WithStack(err)
				return nil, err
			} else if tt != nil {
				if tt.Name == "api-create" || tt.HasOption("api-create") {
					sf.ApiCreate = true
					apiActionsSet = true
				}
				if tt.Name == "api-read" || tt.HasOption("api-read") {
					sf.ApiRead = true
					apiActionsSet = true
				}
				if tt.Name == "api-update" || tt.HasOption("api-update") {
					sf.ApiUpdate = true
					apiActionsSet = true
				}
				if tt.Name == "api-hide" || tt.HasOption("api-hide") {
					sf.ApiHide = true
					apiActionsSet = true
				}
			}

			if !apiActionsSet {
				sf.ApiCreate = true
				sf.ApiRead = true
				sf.ApiUpdate = true
			}
		}

		// Set the column name to the field name if empty and does not equal '-'.
		if sf.ColumnName == "" {
			sf.ColumnName = sf.FieldName
		}

		// If a base model as already been parsed with the db columns,
		// append to the current field.
		if baseModel != nil {
			for _, baseSf := range baseModel.Fields {
				if baseSf.ColumnName == sf.ColumnName {
					sf.DefaultValue = baseSf.DefaultValue
					sf.DbColumn = baseSf.DbColumn
					break
				}
			}
		}

		// Append the field the the model def.
		resp = append(resp, sf)
	}

	return resp, nil
}
