package backupformat

import (
	"fmt"
	"io"
	"strconv"

	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
	"github.com/authzed/spicedb/pkg/namespace"
	core "github.com/authzed/spicedb/pkg/proto/core/v1"
	"github.com/authzed/spicedb/pkg/schemadsl/compiler"
	"github.com/authzed/spicedb/pkg/schemadsl/generator"
	"github.com/authzed/spicedb/pkg/schemadsl/input"
	"github.com/authzed/spicedb/pkg/spiceerrors"
	"github.com/authzed/spicedb/pkg/tuple"
)

// RedactionOptions are the options to use when redacting data.
type RedactionOptions struct {
	// RedactDefinitions will redact the definition names.
	RedactDefinitions bool

	// RedactRelations will redact the relation names.
	RedactRelations bool

	// RedactObjectIDs will redact the object IDs.
	RedactObjectIDs bool
}

// RedactionMap is the map of original names to their redacted names.
type RedactionMap struct {
	// Definitions is the map of original definition names to their redacted names.
	Definitions map[string]string

	// Caveats is the map of original caveat names to their redacted names.
	Caveats map[string]string

	// Relations is the map of original relation names to their redacted names.
	Relations map[string]string

	// ObjectIDs is the map of original object IDs to their redacted names.
	ObjectIDs map[string]string
}

// Invert returns the inverted redaction map, with the redacted names as the keys.
func (rm RedactionMap) Invert() RedactionMap {
	inverted := RedactionMap{
		Definitions: make(map[string]string),
		Caveats:     make(map[string]string),
		Relations:   make(map[string]string),
		ObjectIDs:   make(map[string]string),
	}

	for k, v := range rm.Definitions {
		inverted.Definitions[v] = k
	}

	for k, v := range rm.Caveats {
		inverted.Caveats[v] = k
	}

	for k, v := range rm.Relations {
		inverted.Relations[v] = k
	}

	for k, v := range rm.ObjectIDs {
		inverted.ObjectIDs[v] = k
	}

	return inverted
}

// NewRedactor creates a new redactor that will redact the data as it is written.
func NewRedactor(dec *Decoder, w io.Writer, opts RedactionOptions) (*Redactor, error) {
	// Rewrite the schema to redact as requested.
	redactedSchema, redactionMap, err := redactSchema(dec.Schema(), opts)
	if err != nil {
		return nil, err
	}

	// Create a new encoder with the redacted schema.
	token := dec.ZedToken()
	encoder, err := NewEncoder(w, redactedSchema, token)
	if err != nil {
		return nil, err
	}

	return &Redactor{dec, opts, encoder, redactionMap}, nil
}

type Redactor struct {
	dec          *Decoder
	opts         RedactionOptions
	enc          *Encoder
	redactionMap RedactionMap
}

// Next redacts the next record and writes it to the writer.
func (r *Redactor) Next() error {
	// Read the next record.
	rel, err := r.dec.Next()
	if err != nil {
		return err
	}

	if rel == nil {
		return io.EOF
	}

	// Redact the record.
	redactedRel, err := redactRelationship(rel, &r.redactionMap, r.opts)
	if err != nil {
		return err
	}

	// Write the redacted record.
	return r.enc.Append(redactedRel)
}

// RedactionMap returns the redaction map containing the original names and their redacted names.
func (r *Redactor) RedactionMap() RedactionMap {
	return r.redactionMap
}

func (r *Redactor) Close() error {
	if err := r.enc.Close(); err != nil {
		return err
	}

	return r.dec.Close()
}

func redactSchema(schema string, opts RedactionOptions) (string, RedactionMap, error) {
	// Parse the schema.
	compiled, err := compiler.Compile(compiler.InputSchema{
		Source:       input.Source("schema"),
		SchemaString: schema,
	}, compiler.AllowUnprefixedObjectType())
	if err != nil {
		return "", RedactionMap{}, err
	}

	// Create a new schema with the redacted fields.
	redactionMap := RedactionMap{
		Definitions: make(map[string]string),
		Caveats:     make(map[string]string),
		Relations:   make(map[string]string),
		ObjectIDs:   make(map[string]string),
	}

	redactionCount := 0

	// Redact namespace and caveat names.
	if opts.RedactDefinitions {
		for _, nsDef := range compiled.ObjectDefinitions {
			if opts.RedactDefinitions {
				redactionMap.Definitions[nsDef.Name] = "def" + strconv.Itoa(redactionCount)
				redactionCount++
				nsDef.Name = redactionMap.Definitions[nsDef.Name]
			}

			namespace.FilterUserDefinedMetadataInPlace(nsDef)
		}

		if len(compiled.CaveatDefinitions) > 0 {
			fmt.Println("WARNING: Caveat parameters and comments are not currently redacted.")
		}

		for _, caveatDef := range compiled.CaveatDefinitions {
			if opts.RedactDefinitions {
				redactionMap.Caveats[caveatDef.Name] = "cav" + strconv.Itoa(redactionCount)
				redactionCount++
				caveatDef.Name = redactionMap.Caveats[caveatDef.Name]
			}

			// TODO: Redact caveat parameters.
			// TODO: filter caveat metadata.
		}
	}

	// Redact relation names.
	if opts.RedactRelations {
		for _, nsDef := range compiled.ObjectDefinitions {
			for _, relDef := range nsDef.Relation {
				if existing, ok := redactionMap.Relations[relDef.Name]; ok {
					relDef.Name = existing
					continue
				}

				redactionMap.Relations[relDef.Name] = "rel" + strconv.Itoa(redactionCount)
				redactionCount++
				relDef.Name = redactionMap.Relations[relDef.Name]
			}
		}
	}

	// Redact type information.
	if opts.RedactDefinitions || opts.RedactRelations {
		for _, nsDef := range compiled.ObjectDefinitions {
			for _, relDef := range nsDef.Relation {
				if relDef.TypeInformation != nil {
					for _, allowedDirect := range relDef.TypeInformation.AllowedDirectRelations {
						if opts.RedactDefinitions {
							allowedDirect.Namespace = redactionMap.Definitions[allowedDirect.Namespace]

							if allowedDirect.RequiredCaveat != nil {
								allowedDirect.RequiredCaveat.CaveatName = redactionMap.Caveats[allowedDirect.RequiredCaveat.CaveatName]
							}
						}

						if opts.RedactRelations {
							switch t := allowedDirect.RelationOrWildcard.(type) {
							case *core.AllowedRelation_Relation:
								t.Relation = redactionMap.Relations[t.Relation]
							}
						}
					}
				}
			}
		}
	}

	// Redact within userset rewrites.
	if opts.RedactRelations {
		for _, nsDef := range compiled.ObjectDefinitions {
			for _, relDef := range nsDef.Relation {
				if relDef.UsersetRewrite != nil {
					err := redactUsersetRewrite(relDef.UsersetRewrite, &redactionMap)
					if err != nil {
						return "", RedactionMap{}, err
					}
				}
			}
		}
	}

	// Generate the schema string.
	generated, _, err := generator.GenerateSchema(compiled.OrderedDefinitions)
	return generated, redactionMap, err
}

func redactUsersetRewrite(usersetRewrite *core.UsersetRewrite, redactionMap *RedactionMap) error {
	switch t := usersetRewrite.RewriteOperation.(type) {
	case *core.UsersetRewrite_Union:
		return redactRewriteChildren(t.Union.Child, redactionMap)

	case *core.UsersetRewrite_Intersection:
		return redactRewriteChildren(t.Intersection.Child, redactionMap)

	case *core.UsersetRewrite_Exclusion:
		return redactRewriteChildren(t.Exclusion.Child, redactionMap)

	default:
		return spiceerrors.MustBugf("unknown userset rewrite type: %T", t)
	}
}

func redactRewriteChildren(children []*core.SetOperation_Child, redactionMap *RedactionMap) error {
	for _, child := range children {
		switch t := child.ChildType.(type) {
		case *core.SetOperation_Child_ComputedUserset:
			t.ComputedUserset.Relation = redactionMap.Relations[t.ComputedUserset.Relation]

		case *core.SetOperation_Child_UsersetRewrite:
			err := redactUsersetRewrite(t.UsersetRewrite, redactionMap)
			if err != nil {
				return err
			}

		case *core.SetOperation_Child_TupleToUserset:
			t.TupleToUserset.Tupleset.Relation = redactionMap.Relations[t.TupleToUserset.Tupleset.Relation]
			t.TupleToUserset.ComputedUserset.Relation = redactionMap.Relations[t.TupleToUserset.ComputedUserset.Relation]

		case *core.SetOperation_Child_XNil:
			// nothing to do

		case *core.SetOperation_Child_XThis:
			// nothing to do

		default:
			return spiceerrors.MustBugf("unknown child type: %T", t)
		}
	}

	return nil
}

func redactRelationship(rel *v1.Relationship, redactionMap *RedactionMap, opts RedactionOptions) (*v1.Relationship, error) {
	redactedRel := rel.CloneVT()

	// Redact the resource.
	if opts.RedactDefinitions {
		redactedRel.Resource.ObjectType = redactionMap.Definitions[redactedRel.Resource.ObjectType]
		redactedRel.Subject.Object.ObjectType = redactionMap.Definitions[redactedRel.Subject.Object.ObjectType]

		if rel.OptionalCaveat != nil {
			redactedRel.OptionalCaveat.CaveatName = redactionMap.Caveats[redactedRel.OptionalCaveat.CaveatName]
		}
	}

	// Redact the relation.
	if opts.RedactRelations {
		redactedRel.Relation = redactionMap.Relations[redactedRel.Relation]

		if rel.Subject.OptionalRelation != "" {
			redactedRel.Subject.OptionalRelation = redactionMap.Relations[redactedRel.Subject.OptionalRelation]
		}
	}

	// Redact the object IDs.
	if opts.RedactObjectIDs {
		redactionMap.ObjectIDs[tuple.PublicWildcard] = tuple.PublicWildcard // wilcards are not redacted
		if _, ok := redactionMap.ObjectIDs[redactedRel.Resource.ObjectId]; !ok {
			if redactedRel.Resource.ObjectId != tuple.PublicWildcard {
				redactionMap.ObjectIDs[redactedRel.Resource.ObjectId] = "obj" + strconv.Itoa(len(redactionMap.ObjectIDs))
			}
		}

		redactedRel.Resource.ObjectId = redactionMap.ObjectIDs[redactedRel.Resource.ObjectId]

		if _, ok := redactionMap.ObjectIDs[redactedRel.Subject.Object.ObjectId]; !ok {
			redactionMap.ObjectIDs[redactedRel.Subject.Object.ObjectId] = "obj" + strconv.Itoa(len(redactionMap.ObjectIDs))
		}

		redactedRel.Subject.Object.ObjectId = redactionMap.ObjectIDs[redactedRel.Subject.Object.ObjectId]
	}

	return redactedRel, nil
}
