//
//  AppPermission.swift
//  AltStore
//
//  Created by Riley Testut on 7/23/19.
//  Copyright © 2019 Riley Testut. All rights reserved.
//

import CoreData
import UIKit

import AltSign

@objc(AppPermission) @dynamicMemberLookup
public class AppPermission: BaseEntity
{
    /* Properties */
    @NSManaged public var type: ALTAppPermissionType
    
    // usageDescription must be non-optional for backwards compatibility,
    // so we store non-optional value and provide public accessor with optional return type.
    @nonobjc public var usageDescription: String? {
        get { _usageDescription.isEmpty ? nil : _usageDescription }
        set { _usageDescription = newValue ?? "" }
    }
    @NSManaged @objc(usageDescription) private var _usageDescription: String
    
    @nonobjc public var permission: any ALTAppPermission {
        switch self.type
        {
        case .entitlement: return ALTEntitlement(rawValue: self._permission)
        case .privacy: return ALTAppPrivacyPermission(rawValue: self._permission)
        default: return UnknownAppPermission(rawValue: self._permission)
        }
    }
    @NSManaged @objc(permission) private(set) public var _permission: String
    
    // Set by StoreApp.
    @NSManaged public var appBundleID: String
    @NSManaged public var sourceID: String
    
    /* Relationships */
    @NSManaged public internal(set) var app: StoreApp?
    
    private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
    {
        super.init(entity: entity, insertInto: context)
    }
    
    convenience init(permission: String, usageDescription: String?, type: ALTAppPermissionType, context: NSManagedObjectContext)
    {
        self.init(entity: AppPermission.entity(), insertInto: context)
        
        self._permission = permission
        self.usageDescription = usageDescription
        self.type = type
    }
}

public extension AppPermission
{
    @nonobjc class func fetchRequest() -> NSFetchRequest<AppPermission>
    {
        return NSFetchRequest<AppPermission>(entityName: "AppPermission")
    }
}

// @dynamicMemberLookup
public extension AppPermission
{
    // Convenience for accessing .permission properties.
    subscript<T>(dynamicMember keyPath: KeyPath<any ALTAppPermission, T>) -> T {
        get {
            return self.permission[keyPath: keyPath]
        }
    }
}

private struct AnyDecodable: Decodable
{
    init(from decoder: Decoder) throws 
    {
    }
}

internal struct AppPermissions: Decodable
{
    var entitlements: [AppPermission] = []
    var privacy: [AppPermission] = []
    
    private enum CodingKeys: String, CodingKey, Decodable
    {
        case entitlements
        case privacy
        
        // Legacy
        case name
        case usageDescription
    }
    
    init(from decoder: Decoder) throws
    {
        guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
        
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.entitlements = try self.parseEntitlements(from: container, into: context)
        self.privacy = try self.parsePrivacyPermissions(from: container, into: context)
    }
    
    private func parseEntitlements(from container: KeyedDecodingContainer<CodingKeys>, into context: NSManagedObjectContext) throws -> [AppPermission]
    {
        guard container.contains(.entitlements) else { return [] }
        
        do
        {
            do
            {
                // Legacy
                // Must parse as [String: String], NOT [CodingKeys: String], to avoid incorrect DecodingError.typeMismatch error.
                let rawEntitlements = try container.decode([[String: String]].self, forKey: .entitlements)
                
                let entitlements = try rawEntitlements.compactMap { (dictionary) -> AppPermission? in
                    guard let name = dictionary[CodingKeys.name.rawValue] else {
                        let context = DecodingError.Context(codingPath: container.codingPath, debugDescription: "Legacy entitlements must have `name` key.")
                        throw DecodingError.keyNotFound(CodingKeys.name, context)
                    }
                    
                    let entitlement = AppPermission(permission: name, usageDescription: nil, type: .entitlement, context: context)
                    return entitlement
                }
                
                return entitlements
            }
            catch DecodingError.typeMismatch
            {
                // Detailed
                // AnyDecodable ensures we're forward-compatible with any values we may later require for entitlement permissions.
                let rawEntitlements = try container.decode([String: AnyDecodable?].self, forKey: .entitlements)
                
                let entitlements = rawEntitlements.map { AppPermission(permission: $0.key, usageDescription: nil, type: .entitlement, context: context) }
                return entitlements
            }
        }
        catch DecodingError.typeMismatch
        {
            // Default
            let rawEntitlements = try container.decode([String].self, forKey: .entitlements)
            
            let entitlements = rawEntitlements.map { AppPermission(permission: $0, usageDescription: nil, type: .entitlement, context: context) }
            return entitlements
        }
    }
    
    private func parsePrivacyPermissions(from container: KeyedDecodingContainer<CodingKeys>, into context: NSManagedObjectContext) throws -> [AppPermission]
    {
        guard container.contains(.privacy) else { return [] }
        
        do
        {
            // Legacy
            // Must parse as [String: String], NOT [CodingKeys: String], to avoid incorrect DecodingError.typeMismatch error.
            let rawPermissions = try container.decode([[String: String]].self, forKey: .privacy)
            
            let permissions = try rawPermissions.compactMap { (dictionary) -> AppPermission? in
                guard let name = dictionary[CodingKeys.name.rawValue] else {
                    let context = DecodingError.Context(codingPath: container.codingPath, debugDescription: "Legacy privacy permissions must have `name` key.")
                    throw DecodingError.keyNotFound(CodingKeys.name, context)
                }
                
                let usageDescription = dictionary[CodingKeys.usageDescription.rawValue]
                
                let convertedName = "NS" + name + "UsageDescription" // Convert legacy privacy permissions to their NS[Privacy]UsageDescription equivalent.
                let permission = AppPermission(permission: convertedName, usageDescription: usageDescription, type: .privacy, context: context)
                return permission
            }
            
            return permissions
        }
        catch DecodingError.typeMismatch
        {
            // Default
            let rawPermissions = try container.decode([String: String?].self, forKey: .privacy)
            
            let permissions = rawPermissions.map { AppPermission(permission: $0, usageDescription: $1, type: .privacy, context: context) }
            return permissions
        }
    }
}
