// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package schema

import (
	"bytes"
	"context"
	"encoding/json"

	"github.com/pkg/errors"

	"github.com/ory/jsonschema/v3"
	"github.com/ory/kratos/embedx"
	"github.com/ory/x/jsonschemax"
)

const (
	ExtensionName string = "ory.sh/kratos"
)

type (
	ExtensionConfig struct {
		Credentials struct {
			Password struct {
				Identifier bool `json:"identifier"`
			} `json:"password"`
			WebAuthn struct {
				Identifier bool `json:"identifier"`
			} `json:"webauthn"`
			Passkey struct {
				DisplayName bool `json:"display_name"`
			} `json:"passkey"`
			TOTP struct {
				AccountName bool `json:"account_name"`
			} `json:"totp"`
			Code struct {
				Identifier bool   `json:"identifier"`
				Via        string `json:"via"`
			} `json:"code"`
		} `json:"credentials"`
		Verification struct {
			Via string `json:"via"`
		} `json:"verification"`
		Recovery struct {
			Via string `json:"via"`
		} `json:"recovery"`
		RawSchema map[string]interface{} `json:"-"`
	}

	ValidateExtension interface {
		Run(ctx jsonschema.ValidationContext, config ExtensionConfig, value interface{}) error
		Finish() error
	}
	CompileExtension interface {
		Run(ctx jsonschema.CompilerContext, config ExtensionConfig, rawSchema map[string]interface{}) error
	}

	ExtensionRunner struct {
		meta     *jsonschema.Schema
		compile  func(ctx jsonschema.CompilerContext, rawSchema map[string]interface{}) (interface{}, error)
		validate func(ctx jsonschema.ValidationContext, s interface{}, v interface{}) error

		validateRunners []ValidateExtension
		compileRunners  []CompileExtension
	}

	ExtensionRunnerOption func(*ExtensionRunner)
)

func (e *ExtensionConfig) EnhancePath(path jsonschemax.Path) map[string]any {
	props := path.CustomProperties
	if props == nil {
		props = make(map[string]any)
	}
	props[ExtensionName] = e

	return props
}

func WithValidateRunners(runners ...ValidateExtension) ExtensionRunnerOption {
	return func(r *ExtensionRunner) {
		r.validateRunners = append(r.validateRunners, runners...)
	}
}

func WithCompileRunners(runners ...CompileExtension) ExtensionRunnerOption {
	return func(r *ExtensionRunner) {
		r.compileRunners = append(r.compileRunners, runners...)
	}
}

func NewExtensionRunner(ctx context.Context, opts ...ExtensionRunnerOption) (*ExtensionRunner, error) {
	var err error
	r := new(ExtensionRunner)
	c := jsonschema.NewCompiler()

	if err = embedx.AddSchemaResources(c, embedx.IdentityExtension); err != nil {
		return nil, errors.WithStack(err)
	}

	r.meta, err = c.Compile(ctx, embedx.IdentityExtension.GetSchemaID())
	if err != nil {
		return nil, errors.WithStack(err)
	}

	r.compile = func(ctx jsonschema.CompilerContext, m map[string]interface{}) (interface{}, error) {
		if raw, ok := m[ExtensionName]; ok {
			var b bytes.Buffer
			if err := json.NewEncoder(&b).Encode(raw); err != nil {
				return nil, errors.WithStack(err)
			}

			var e ExtensionConfig
			if err := json.NewDecoder(&b).Decode(&e); err != nil {
				return nil, errors.WithStack(err)
			}

			for _, runner := range r.compileRunners {
				if err := runner.Run(ctx, e, m); err != nil {
					return nil, err
				}
			}
			e.RawSchema = m

			return &e, nil
		}
		return nil, nil
	}

	r.validate = func(ctx jsonschema.ValidationContext, s interface{}, v interface{}) error {
		c, ok := s.(*ExtensionConfig)
		if !ok {
			return nil
		}

		for _, runner := range r.validateRunners {
			if err := runner.Run(ctx, *c, v); err != nil {
				return err
			}
		}
		return nil
	}

	for _, opt := range opts {
		opt(r)
	}

	return r, nil
}

func (r *ExtensionRunner) Register(compiler *jsonschema.Compiler) *ExtensionRunner {
	compiler.Extensions[ExtensionName] = r.Extension()
	return r
}

func (r *ExtensionRunner) Extension() jsonschema.Extension {
	return jsonschema.Extension{
		Meta:     r.meta,
		Compile:  r.compile,
		Validate: r.validate,
	}
}

func (r *ExtensionRunner) AddRunner(run ValidateExtension) *ExtensionRunner {
	r.validateRunners = append(r.validateRunners, run)
	return r
}

func (r *ExtensionRunner) Finish() error {
	for _, runner := range r.validateRunners {
		if err := runner.Finish(); err != nil {
			return err
		}
	}
	return nil
}
