package model

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"

	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/common/types"
	"gorm.io/gorm"

	"github.com/tidbcloud/tidb-management-service/functional-test/cases/openapidedicated/autogencase/utils"
)

const (
	RequestTypeHTTP       = "http"
	RequestTypeSDKAWS     = "sdk_aws"
	RequestTypeSDKGCP     = "sdk_gcp"
	RequestTypeSDKAzure   = "sdk_azure"
	RequestTypeCMDAWS     = "cmd_aws"
	RequestTypeCMDGCP     = "cmd_gcp"
	RequestTypeCMDAzure   = "cmd_azure"
	RequestTypeCMDMysqlsh = "cmd_mysqlsh"
	RequestTypeCMDALIYUN  = "cmd_aliyun"
)

const (
	AuthTypeDigest      = "digest"
	AuthTypeBearerToken = "bearer_token" // TODO: support bearer token
)

type AuthContent struct {
	PublicKey  string `json:"public_key"  yaml:"public_key"`
	PrivateKey string `json:"private_key" yaml:"private_key"`
}

type HTTPRequestAuth struct {
	AuthType string       `json:"auth_type" yaml:"auth_type"`
	Content  *AuthContent `json:"content"   yaml:"content"`
	AuthRole string       `json:"auth_role" yaml:"auth_role"`
}

type HTTPRequestSpec struct {
	Auth        *HTTPRequestAuth  `json:"auth"         yaml:"auth"`
	Method      string            `json:"method"       yaml:"method"`
	Path        string            `json:"path"         yaml:"path"`
	PathParams  map[string]string `json:"path_params"  yaml:"path_params"`
	QueryParams json.RawMessage   `json:"query_params" yaml:"query_params"`
	Headers     map[string]string `json:"headers"      yaml:"headers"`
	Body        json.RawMessage   `json:"body"         yaml:"body"`
	BodyParams  map[string]string `json:"body_params"  yaml:"body_params"`
}

type SaveExpression struct {
	Key  string `json:"key"  yaml:"key"`
	Eval string `json:"eval" yaml:"eval"`
}

type SaveSpec struct {
	Placeholder []SaveExpression `json:"placeholder" yaml:"placeholder"`
}

// TODO: fix spec
type HTTPResponseExpect struct {
	StatusCode *int   `json:"status_code,omitempty" yaml:"status_code"`
	CELExpr    string `json:"cel"                   yaml:"cel"`
}

func (e *HTTPResponseExpect) String() string {
	buf := bytes.Buffer{}
	enc := json.NewEncoder(&buf)
	enc.SetEscapeHTML(false)
	_ = enc.Encode(e)
	return buf.String()
}

func (e *HTTPResponseExpect) IsExpected(resp *http.Response) (bool, error) {
	if e.StatusCode != nil && *e.StatusCode != resp.StatusCode {
		return false, nil
	}
	isExpected := true
	if eval := e.CELExpr; eval != "" {
		if rseeker, ok := resp.Body.(io.ReadSeeker); ok {
			_, _ = rseeker.Seek(0, 0)
		}
		bs, err := io.ReadAll(resp.Body)
		if err != nil {
			return false, err
		}
		celVars, celEnvOpt, err := utils.ParseHTTPBodyToCElVars(bs)
		if err != nil {
			return false, err
		}
		env, err := cel.NewEnv(celEnvOpt)
		if err != nil {
			return false, err
		}
		ast, issues := env.Compile(eval)
		if issues != nil && issues.Err() != nil {
			return false, issues.Err()
		}
		prg, err := env.Program(ast)
		if err != nil {
			return false, err
		}
		out, _, err := prg.Eval(celVars)
		if err != nil {
			return false, err
		}
		switch v := out.(type) {
		case types.Bool:
			isExpected = bool(v)
		default:
			return false, fmt.Errorf("cannot conver type %T to bool", v)
		}
	}
	return isExpected, nil
}

type CommandSpec struct {
	Name       string            `json:"name"        yaml:"name"`
	Args       []string          `json:"args"        yaml:"args"`
	ArgsParams map[string]string `json:"args_params" yaml:"args_params"`
}

type CommandExpect struct {
	ExitCode *int   `json:"exit_code,omitempty" yaml:"exit_code"`
	CELExpr  string `json:"cel"                 yaml:"cel"`
}

func (e *CommandExpect) String() string {
	buf := bytes.Buffer{}
	enc := json.NewEncoder(&buf)
	enc.SetEscapeHTML(false)
	_ = enc.Encode(e)
	return buf.String()
}

func (e *CommandExpect) IsExpected(exitCode int, bs []byte) (bool, error) {
	if e.ExitCode != nil && *e.ExitCode != exitCode {
		return false, nil
	}
	isExpected := true
	if eval := e.CELExpr; eval != "" {
		celVars, celEnvOpt, err := utils.ParseCMDOutputToCElVars(bs)
		if err != nil {
			return false, err
		}
		env, err := cel.NewEnv(celEnvOpt)
		if err != nil {
			return false, err
		}
		ast, issues := env.Compile(eval)
		if issues != nil && issues.Err() != nil {
			return false, issues.Err()
		}
		prg, err := env.Program(ast)
		if err != nil {
			return false, err
		}
		out, _, err := prg.Eval(celVars)
		if err != nil {
			return false, err
		}
		switch v := out.(type) {
		case types.Bool:
			isExpected = bool(v)
		default:
			return false, fmt.Errorf("cannot conver type %T to bool", v)
		}
	}
	return isExpected, nil
}

// type Stage string
//
// const (
// 	Prepare  Stage = "prepare"
// 	Execute  Stage = "execute"
// 	Teardown Stage = "teardown"
// )

type ScenarioFile struct {
	Scenario Scenario         `json:"scenario" yaml:"scenario"`
	Steps    []GenerationStep `json:"steps"    yaml:"steps"`
}

type Scenario struct {
	ID          string          `json:"id"          yaml:"id"`
	Context     ScenarioContext `json:"context"     yaml:"context"`
	Description string          `json:"description" yaml:"description"`
	Name        string          `json:"name"        yaml:"name"`
}

type ScenarioContext struct {
	BasePath string `json:"base_path" yaml:"base_path"`
	Host     string `json:"host"      yaml:"host"`
}

type GenerationStep struct {
	CaseID       string  `gorm:"type:VARCHAR(64);not null"  json:"case_id"       yaml:"case_id"`
	Name         string  `gorm:"type:VARCHAR(128);not null" json:"name"          yaml:"name"`
	OperationID  string  `gorm:"type:VARCHAR(64);not null"  json:"operation_id"  yaml:"operation_id"`
	GenerationID string  `gorm:"type:VARCHAR(64);not null"  json:"generation_id" yaml:"generation_id"`
	Order        float64 `gorm:"type:DOUBLE;not null"       json:"order"         yaml:"order"`
	StepType     string  `gorm:"type:VARCHAR(64);not null"  json:"step_type"     yaml:"step_type"`
	Stage        string  `gorm:"type:VARCHAR(16);not null"  json:"stage"         yaml:"stage"`
	Meta         *string `gorm:"type:JSON;"                 json:"meta"          yaml:"meta"`

	RequestType          string  `gorm:"type:VARCHAR(64);not null" json:"request_type"           yaml:"request_type"`
	Request              *string `gorm:"type:JSON;"                json:"request"                yaml:"request"`
	MaxRetries           int     `gorm:"not null"                  json:"max_retries"            yaml:"max_retries"`
	DelayAfter           int     `gorm:"not null"                  json:"delay_after"            yaml:"delay_after"`
	Expect               *string `gorm:"type:JSON;"                json:"expect"                 yaml:"expect"`
	Save                 *string `gorm:"type:JSON;"                json:"save"                   yaml:"save"`
	ContinueOnUnexpected bool    `gorm:"type:BOOL;not null"        json:"continue_on_unexpected" yaml:"continue_on_unexpected"`

	// TraceID            string  `gorm:"type:VARCHAR(64);not null"`
	// OperationID        string  `gorm:"type:VARCHAR(64);not null"`
	// Category           string  `gorm:"type:VARCHAR(64);not null"`
	// AffectedElement    string  `gorm:"type:VARCHAR(64);not null"`
	// ElementType        string  `gorm:"type:VARCHAR(64);not null"`
	// GenerationStrategy *string `gorm:"type:VARCHAR(64)"`
	// Method             string  `gorm:"type:VARCHAR(16);not null"`
	// Path               string  `gorm:"type:VARCHAR(256);not null"`
	// Environment        string  `gorm:"type:JSON;not null"`
	// ExpectedStatus     int     `gorm:"not null"`
	// ServiceName        string  `gorm:"type:VARCHAR(64);not null"`
	// SpecCommitID       string  `gorm:"type:VARCHAR(64);not null"`
	// Ignored            bool    `gorm:"not null"`
	// Type               string  `gorm:"type:VARCHAR(64);not null"`
	// SpecPRURL          string  `gorm:"column:spec_pr_url;type:VARCHAR(64);not null"`
}

func (step *GenerationStep) ParseHTTPRequestSpec() (*HTTPRequestSpec, error) {
	if step.RequestType != RequestTypeHTTP {
		return nil, fmt.Errorf("can not parse HTTPRequestSpec for RequestType %s", step.RequestType)
	}
	if step.Request == nil {
		return nil, errors.New("can not parse HTTPRequestSpec for nil Request")
	}
	if *step.Request == "" {
		return nil, errors.New("can not parse HTTPRequestSpec for empty Request")
	}
	spec := new(HTTPRequestSpec)
	if err := json.Unmarshal([]byte(*step.Request), spec); err != nil {
		return nil, err
	}
	return spec, nil
}

func (step *GenerationStep) ParseHTTPResponseExpect() (*HTTPResponseExpect, error) {
	if step.RequestType != RequestTypeHTTP {
		return nil, fmt.Errorf("can not parse HTTPResponseExpected for RequestType %s", step.RequestType)
	}
	if step.Expect == nil {
		return nil, errors.New("can not parse HTTPResponseExpected for nil Expect")
	}
	if *step.Expect == "" {
		return nil, errors.New("can not parse HTTPRequestSpec for empty Expect")
	}
	expect := new(HTTPResponseExpect)
	if err := json.Unmarshal([]byte(*step.Expect), expect); err != nil {
		return nil, err
	}
	return expect, nil
}

func (step *GenerationStep) ParseSaveSpec() (*SaveSpec, error) {
	spec := new(SaveSpec)
	spec.Placeholder = make([]SaveExpression, 0)
	if step.Save != nil {
		if err := json.Unmarshal([]byte(*step.Save), spec); err != nil {
			return nil, err
		}
	}
	return spec, nil
}

func (step *GenerationStep) ParseCommandSpec() (*CommandSpec, error) {
	if !step.isRequestTypeCommand() {
		return nil, fmt.Errorf("can not parse CommandExpected for RequestType %s", step.RequestType)
	}

	if step.Request == nil {
		return nil, errors.New("can not parse CommandSpec for nil Request")
	}
	if *step.Request == "" {
		return nil, errors.New("can not parse CommandSpec for empty Request")
	}
	spec := new(CommandSpec)
	if err := json.Unmarshal([]byte(*step.Request), spec); err != nil {
		return nil, err
	}
	switch step.RequestType {
	case RequestTypeCMDAWS:
		if spec.Name != "aws" {
			return nil, fmt.Errorf("RequestType %s does not support commandline %s", step.RequestType, spec.Name)
		}
	case RequestTypeCMDGCP:
		if spec.Name != "gcloud" {
			return nil, fmt.Errorf("RequestType %s does not support commandline %s", step.RequestType, spec.Name)
		}
	case RequestTypeCMDAzure:
		if spec.Name != "az" {
			return nil, fmt.Errorf("RequestType %s does not support commandline %s", step.RequestType, spec.Name)
		}
	case RequestTypeCMDMysqlsh:
		if spec.Name != "mysqlsh" {
			return nil, fmt.Errorf("RequestType %s does not support commandline %s", step.RequestType, spec.Name)
		}
	case RequestTypeCMDALIYUN:
		if spec.Name != "aliyun" {
			return nil, fmt.Errorf("RequestType %s does not support commandline %s", step.RequestType, spec.Name)
		}
	}
	return spec, nil
}

func (step *GenerationStep) ParseCommandExpect() (*CommandExpect, error) {
	if !step.isRequestTypeCommand() {
		return nil, fmt.Errorf("can not parse CommandExpected for RequestType %s", step.RequestType)
	}

	if step.Expect == nil {
		return nil, errors.New("can not parse CommandExpected for nil Expect")
	}
	if *step.Expect == "" {
		return nil, errors.New("can not parse HTTPRequestSpec for empty Expect")
	}
	expect := new(CommandExpect)
	if err := json.Unmarshal([]byte(*step.Expect), expect); err != nil {
		return nil, err
	}
	return expect, nil
}

func (step *GenerationStep) isRequestTypeCommand() bool {
	switch step.RequestType {
	case RequestTypeCMDAWS, RequestTypeCMDGCP, RequestTypeCMDAzure, RequestTypeCMDMysqlsh, RequestTypeCMDALIYUN:
		return true
	}
	return false
}

func (GenerationStep) TableName() string {
	return "generation_steps"
}

type GenerationStepDAO struct {
	db *gorm.DB
}

func NewGenerationStepDAO(db *gorm.DB) (*GenerationStepDAO, error) {
	dao := &GenerationStepDAO{
		db: db,
	}
	return dao, nil
}

func (dao *GenerationStepDAO) ListGenerationStep(ctx context.Context, generationID string) ([]GenerationStep, error) {
	selector := dao.db.Model(&GenerationStep{}).Where("generation_id = ?", generationID).Order("order")
	rows, err := selector.Rows()
	if err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	defer rows.Close()

	out := make([]GenerationStep, 0)

	for rows.Next() {
		var step GenerationStep
		if err := dao.db.ScanRows(rows, &step); err != nil {
			return nil, err
		}
		out = append(out, step)
	}
	return out, nil
}
