//
//  JSONSchemaValidationResult.swift
//  DynamicJSON
//
//  Created by Matthias Zenger on 01/04/2024.
//  Copyright © 2024 Matthias Zenger. All rights reserved.
//
//  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.
//

import Foundation


/// Protocol defining type for annotation messages.
public protocol AnnotationMessage {
  func description(value: LocatedJSON, location: JSONLocation) -> String
}

/// Protocol specifying a failure reason.
public protocol FailureReason {
  var reason: String { get }
}

///
/// Result container for JSON schema validators. Currently, `JSONSchemaValidationResult`
/// values primarily collect errors, format and meta annotations, as well as defaults.
///
public struct JSONSchemaValidationResult: CustomStringConvertible {
  
  public struct Annotation<Message: AnnotationMessage>: CustomStringConvertible {
    public let value: LocatedJSON
    public let location: JSONLocation
    public let message: Message
    
    public init(value: LocatedJSON,
                location: JSONLocation,
                message: Message) {
      self.value = value
      self.location = location
      self.message = message
    }
    
    public var description: String {
      return message.description(value: self.value, location: self.location)
    }
  }
  
  public struct ValidationError: AnnotationMessage {
    public let schema: JSONSchema
    public let reason: FailureReason
    
    public func description(value: LocatedJSON, location: JSONLocation) -> String {
      return "value \(value) not matching schema \(self.schema.id?.string ?? "") at \(location); " +
             "reason: \(self.reason.reason)"
    }
  }
  
  public struct MetaTags: OptionSet, AnnotationMessage {
    public static let deprecated = MetaTags(rawValue: 1 << 0)
    public static let readOnly = MetaTags(rawValue: 1 << 1)
    public static let writeOnly = MetaTags(rawValue: 1 << 2)
    
    public let rawValue: UInt
    
    public init(rawValue: UInt = 0) {
      self.rawValue = rawValue
    }
    
    public func description(value: LocatedJSON, location: JSONLocation) -> String {
      var strs: [String] = []
      if self.contains(.deprecated) {
        strs.append("deprecated")
      }
      if self.contains(.readOnly) {
        strs.append("readOnly")
      }
      if self.contains(.writeOnly) {
        strs.append("writeOnly")
      }
      return "meta tags for value \(value) at location \(location): \(strs.joined(separator: ", "))"
    }
  }
  
  public struct FormatConstraint: AnnotationMessage {
    public let format: String
    public let valid: Bool?
    
    public func description(value: LocatedJSON, location: JSONLocation) -> String {
      return "string \(value) needs to conform with format '\(self.format)' at location " +
             "\(location)" + (valid == nil ? "" : valid! ? "; valid" : "; invalid")
    }
  }
  
  public enum DefaultPropagationMode {
    case suppress
    case merge
    case altenative
  }
  
  /// Location of the current validator invocation. At the top level, this is always
  /// `.root`. This location is used internally to merge results.
  private let location: JSONLocation
  
  /// Errors found by the validator.
  public private(set) var errors: [Annotation<ValidationError>]
  
  /// Meta tag annotations denoting what values were deprecated, read-only, or write-only.
  public private(set) var tags: [Annotation<MetaTags>]
  
  /// Format annotations. These are always collected, no matter whether the
  /// `format-annotation` vocabulary is enabled or not. If it is enabled, then the
  /// constraints that are not valid can also be found under `errors`.
  public private(set) var formatConstraints: [Annotation<FormatConstraint>]
  
  /// Default annotations. Set of defined defaults for the validated JSON value.
  /// If `default` is `nil`, then no default was provided. If `default` is the empty
  /// array, the determined defaults contradict each other, i.e. no default exists
  /// which meets all relevant `default` annotations.
  public private(set) var `defaults`: [JSONLocation : (exists: Bool, values: Set<JSON>)]
  
  /// The evaluated properties of an object. Used primarily internally.
  public private(set) var evaluatedProperties: Set<String>
  
  /// The evaluated items of an array. Used primarily internally.
  public private(set) var evaluatedItems: Set<Int>
  
  ///  Initializes a new, empty `JSONSchemaValidationResult` value for the given
  ///  location.
  public init(for location: JSONLocation) {
    self.location = location
    self.errors = []
    self.tags = []
    self.formatConstraints = []
    self.defaults = [:]
    self.evaluatedProperties = []
    self.evaluatedItems = []
  }
  
  /// Did the validator succeed and the value is considered valid? If false, at least
  /// one error was found.
  public var isValid: Bool {
    return self.errors.isEmpty
  }
  
  public var nonexistingDefaults: [JSONLocation : Set<JSON>] {
    var res: [JSONLocation : Set<JSON>] = [:]
    for (location, (exists, defaults)) in self.defaults where !exists {
      res[location] = defaults
    }
    return res
  }
  
  /// Returns a JSON patch object encapsulating all default additions determined by the
  /// validator. If multiple defaults are possible for one location, a random one is chosen
  /// and included in the patch object.
  public var defaultPatch: JSONPatch {
    var operations: [JSONPatchOperation] = []
    for (location, (exists, defaults)) in self.defaults where !exists {
      if let pointer = location.pointer, let `default` = defaults.first {
        operations.append(.add(pointer, `default`))
      }
    }
    return JSONPatch(operations: operations)
  }
  
  /// Used to flag errors by validators.
  public mutating func flag(error reason: FailureReason,
                            for value: LocatedJSON,
                            schema: JSONSchema,
                            at location: JSONLocation) {
    self.errors.append(Annotation(value: value,
                                  location: location,
                                  message: ValidationError(schema: schema, reason: reason)))
  }
  
  /// Used to flag meta tag annotations by validators.
  public mutating func flag(tags: MetaTags,
                            for value: LocatedJSON,
                            schema: JSONSchema,
                            at location: JSONLocation) {
    self.tags.append(Annotation(value: value,
                                location: location,
                                message: tags))
  }
  
  /// Used to flag format annotations by validators.
  public mutating func flag(format: String,
                            valid: Bool?,
                            for value: LocatedJSON,
                            schema: JSONSchema,
                            at location: JSONLocation) {
    self.formatConstraints.append(Annotation(value: value,
                                             location: location,
                                             message: FormatConstraint(format: format, valid: valid)))
  }
  
  /// Used to flag default annotations by validators.
  public mutating func flag(default: JSON,
                            for value: LocatedJSON,
                            schema: JSONSchema,
                            at location: JSONLocation) {
    self.defaults[self.location] = self.merge(default: `default`, exists: value.exists)
  }
  
  /// Used by validators to declare a property to be evaluated.
  public mutating func evaluted(property: String) {
    self.evaluatedProperties.insert(property)
  }
  
  /// Used by validators to declare an array item to be evaluated.
  public mutating func evaluted(item: Int) {
    self.evaluatedItems.insert(item)
  }
  
  /// Merges another `JSONSchemaValidationResult` value into this value, declaring
  /// `item` to be evaluated.
  public mutating func include(_ other: JSONSchemaValidationResult, for item: Int) {
    self.include(other)
    self.evaluted(item: item)
  }
  
  /// Merges another `JSONSchemaValidationResult` value into this value, declaring
  /// `member` to be evaluated.
  public mutating func include(_ other: JSONSchemaValidationResult, for member: String) {
    self.include(other)
    self.evaluted(property: member)
  }
  
  /// Merges another `JSONSchemaValidationResult` value into this value if the other
  /// value is valid, declaring `item` to be evaluated.
  public mutating func include(ifValid other: JSONSchemaValidationResult, for item: Int) -> Bool {
    guard other.isValid else {
      self.merge(defaults: other.defaults, mode: .merge)
      return false
    }
    self.include(other, for: item)
    return true
  }
  
  /// Merges another `JSONSchemaValidationResult` value into this value if the other
  /// value is valid, declaring `member` to be evaluated.
  public mutating func include(ifValid other: JSONSchemaValidationResult, for member: String) -> Bool {
    guard other.isValid else {
      self.merge(defaults: other.defaults, mode: .merge)
      return false
    }
    self.include(other, for: member)
    return true
  }
  
  /// Merges another `JSONSchemaValidationResult` value into this value if the other
  /// value is valid.
  public mutating func include(ifValid other: JSONSchemaValidationResult,
                               propagateDefault: DefaultPropagationMode) -> Bool {
    guard other.isValid else {
      self.merge(defaults: other.defaults, mode: propagateDefault)
      return false
    }
    self.include(other, mode: propagateDefault)
    return true
  }
  
  /// Merges another `JSONSchemaValidationResult` value into this value.
  @discardableResult
  public mutating func include(_ other: JSONSchemaValidationResult,
                               mode: DefaultPropagationMode = .merge) -> JSONSchemaValidationResult {
    self.errors.append(contentsOf: other.errors)
    self.formatConstraints.append(contentsOf: other.formatConstraints)
    self.merge(defaults: other.defaults, mode: mode)
    if self.location == other.location {
      self.evaluatedProperties.formUnion(other.evaluatedProperties)
      self.evaluatedItems.formUnion(other.evaluatedItems)
    }
    return other
  }
  
  /// Merges the value of a `default` keyword into the existing set of defaults
  private mutating func merge(default other: JSON, exists: Bool) -> (Bool, Set<JSON>) {
    if let (cexists, current) = self.defaults[self.location] {
      var new: Set<JSON> = []
      for d in current {
        if let merged = d.merging(value: other) {
          new.insert(merged)
        }
      }
      return (exists || cexists, new)
    } else {
      return (exists, [other])
    }
  }
  
  /// Merges two default sets
  private mutating func merge(_ current: (Bool, Set<JSON>),
                              with others: (Bool, Set<JSON>)?,
                              mode: DefaultPropagationMode) -> (Bool, Set<JSON>) {
    switch mode {
      case .suppress:
        return current
      case .merge:
        if let others {
          var new: Set<JSON> = []
          for d in current.1 {
            for o in others.1 {
              if let merged = d.merging(value: o) {
                new.insert(merged)
              }
            }
          }
          return (current.0 || others.0, new)
        } else {
          return current
        }
      case .altenative:
        if let others {
          var new = current.1
          new.formUnion(others.1)
          return (current.0 || others.0, new)
        } else {
          return current
        }
    }
  }
  
  /// Called by validators to merge default sets (for cases where a full result merging
  /// is not wanted).
  public mutating func merge(defaults others: [JSONLocation : (exists: Bool, values: Set<JSON>)],
                             mode: DefaultPropagationMode) {
    for (location, `default`) in self.defaults {
      self.defaults[location] = self.merge(`default`, with: others[location], mode: mode)
    }
    for (location, `default`) in others where self.defaults[location] == nil {
      self.defaults[location] = `default`
    }
  }
  
  /// Textual description of this results value.
  public var description: String {
    var res = ""
    if self.errors.isEmpty {
      res += "VALID"
    } else {
      res += "INVALID:"
      var i = 0
      for error in self.errors {
        i += 1
        res += "\n  [\(i)] \(error)"
      }
    }
    if !self.formatConstraints.isEmpty {
      res += "\nFORMAT CONSTRAINTS:"
      var i = 0
      for conformance in self.formatConstraints {
        i += 1
        res += "\n  [\(i)] \(conformance)"
      }
    }
    return res
  }
}