// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
enum T {
  Integer
  String
  Object(
    Map[String, T],
    required~ : Array[String]?,
    additionalProperties~ : Bool
  )
  Array(T)
  Enum(Array[Json])
}

///|
pub fn integer() -> T {
  T::Integer
}

///|
pub fn string() -> T {
  T::String
}

///|
pub fn object(
  object : Map[String, T],
  additionalProperties~ : Bool = true,
  required? : Array[String]
) -> T {
  T::Object(object, required~, additionalProperties~)
}

///|
pub fn enums(values : Array[Json]) -> T {
  T::Enum(values)
}

///|
pub fn array(schema : T) -> T {
  T::Array(schema)
}

///|
pub fn T::to_json(self : T) -> Json {
  match self {
    T::Integer => { "type": "integer" }
    T::String => { "type": "string" }
    T::Enum(values) => { "enum": Json::array(values) }
    T::Array(schema) => {
      let result : Map[String, Json] = {
        "type": "array",
        "items": schema.to_json(),
      }
      Json::object(result)
    }
    T::Object(map, required~, additionalProperties~) => {
      let json_object = Map::new()
      for key, value in map {
        json_object[key] = value.to_json()
      }
      let result : Map[String, Json] = {
        "type": "object",
        "properties": Json::object(json_object),
      }
      if required is Some(required) {
        result["required"] = Json::array(required.map(Json::string))
      }
      if not(additionalProperties) {
        result["additionalProperties"] = false
      }
      Json::object(result)
    }
  }
}

///|
pub impl ToJson for T with to_json(self) {
  self.to_json()
}

///|
pub fn T::verify(self : T, json : Json) -> Bool {
  match (self, json) {
    (T::Integer, Number(n)) => n == n && n.to_int().to_double() == n
    (T::String, String(_)) => true
    (T::Enum(values), _) => values.contains(json)
    (T::Array(schema), Array(values)) => values.iter().all(schema.verify(_))
    (T::Object(map, required~, additionalProperties~), Object(json)) => {
      let required = @sorted_set.from_array(required.or([]))
      for key, value in map {
        if json.get(key) is Some(json) {
          if not(value.verify(json)) {
            return false
          }
        } else if required.contains(key) {
          return false
        }
      }
      for key, _ in json {
        if not(map.contains(key)) && not(additionalProperties) {
          return false
        }
      }
      true
    }
    _ => false
  }
}

///|
test "to_json" {
  @json.inspect(
    object({
      "name": object({
        "firstName": string(),
        "lastName": string(),
        "middleName": string(),
      }),
    }),
    content={
      "type": "object",
      "properties": {
        "name": {
          "type": "object",
          "properties": {
            "firstName": { "type": "string" },
            "lastName": { "type": "string" },
            "middleName": { "type": "string" },
          },
        },
      },
    },
  )
  @json.inspect(
    object({
      "name": object(
        { "firstName": string(), "lastName": string(), "middleName": string() },
        required=["firstName", "lastName"],
      ),
      "age": integer(),
    }),
    content={
      "type": "object",
      "properties": {
        "name": {
          "type": "object",
          "properties": {
            "firstName": { "type": "string" },
            "lastName": { "type": "string" },
            "middleName": { "type": "string" },
          },
          "required": ["firstName", "lastName"],
        },
        "age": { "type": "integer" },
      },
    },
  )
}

///|
test "verify" {
  let schema = object({
    "name": object({
      "firstName": string(),
      "lastName": string(),
      "middleName": string(),
    }),
  })
  assert_true(
    schema.verify({
      "name": { "firstName": "John", "lastName": "Doe", "middleName": "Smith" },
    }),
  )
  assert_false(
    schema.verify({ "name": { "firstName": "John", "lastName": 1 } }),
  )
  let schema = object({
    "name": object(
      { "firstName": string(), "lastName": string(), "middleName": string() },
      required=["firstName", "lastName"],
    ),
    "age": integer(),
  })
  assert_true(
    schema.verify({
      "name": { "firstName": "John", "lastName": "Doe" },
      "age": 30,
    }),
  )
  assert_false(schema.verify({ "name": { "firstName": "John" }, "age": 30 }))
}