// Copyright 2023 The OPA Authors.  All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package compiler

import (
	"errors"
	"sync"

	"github.com/open-policy-agent/opa/v1/ast"
	"github.com/open-policy-agent/opa/v1/schemas"
	"github.com/open-policy-agent/opa/v1/util"
)

type SchemaFile string

const (
	AuthorizationPolicySchema SchemaFile = "authorizationPolicy.json"
)

var schemaDefinitions = map[SchemaFile]any{}

var loadOnce = sync.OnceValue(func() error {
	cont, err := schemas.FS.ReadFile(string(AuthorizationPolicySchema))
	if err != nil {
		return err
	}

	if len(cont) == 0 {
		return errors.New("expected authorization policy schema file to be present")
	}

	var schema any
	if err := util.Unmarshal(cont, &schema); err != nil {
		return err
	}

	schemaDefinitions[AuthorizationPolicySchema] = schema

	return nil
})

// VerifyAuthorizationPolicySchema performs type checking on rules against the schema for the Authorization Policy
// Input document.
// NOTE: The provided compiler should have already run the compilation process on the input modules
func VerifyAuthorizationPolicySchema(compiler *ast.Compiler, ref ast.Ref) error {
	if err := loadOnce(); err != nil {
		panic(err)
	}

	rules := getRulesWithDependencies(compiler, ref)

	if len(rules) == 0 {
		return nil
	}

	schemaSet := ast.NewSchemaSet()
	schemaSet.Put(ast.SchemaRootRef, schemaDefinitions[AuthorizationPolicySchema])

	errs := ast.NewCompiler().
		WithDefaultRegoVersion(compiler.DefaultRegoVersion()).
		WithSchemas(schemaSet).
		PassesTypeCheckRules(rules)

	if len(errs) > 0 {
		return errs
	}

	return nil
}

// getRulesWithDependencies returns a slice of rules that are referred to by ref along with their dependencies
func getRulesWithDependencies(compiler *ast.Compiler, ref ast.Ref) []*ast.Rule {
	allRules := compiler.GetRules(ref)

	deps := map[*ast.Rule]struct{}{}
	for _, rule := range allRules {
		transitiveDependencies(compiler, rule, deps)
	}

	for dep := range deps {
		allRules = append(allRules, dep)
	}

	return allRules
}

func transitiveDependencies(compiler *ast.Compiler, rule *ast.Rule, deps map[*ast.Rule]struct{}) {
	for x := range compiler.Graph.Dependencies(rule) {
		other := x.(*ast.Rule)
		deps[other] = struct{}{}
		transitiveDependencies(compiler, other, deps)
	}
}
