//
//  MBIMBridge.m
//  MessagesBridge
//
//  Created by James Magahern on 11/12/18.
//  Copyright © 2018 James Magahern. All rights reserved.
//

#import "MBIMBridge.h"
#import "MBIMBridge_Private.h"
#import "MBIMBridgeOperation.h"
#import "MBIMConcurrentHTTPServer.h"
#import "MBIMHTTPConnection.h"
#import "MBIMUpdateQueue.h"
#import "hooking.h"

#import "HTTPServer.h"

#import "IMCore_ClassDump.h"
#import "IMFoundation_ClassDump.h"

static const UInt16 kDefaultPort = 5738;

static NSString *const MBIMBridgeToken = @"net.buzzert.kordophone";

@interface MBIMBridge (/* INTERNAL */) {
    __strong NSArray *_sslCertificateAndIdentity;
}

@property (nonatomic, strong) MBIMConcurrentHTTPServer *httpServer;
@property (nonatomic, strong) NSOperationQueue *operationQueue;

- (instancetype)_init;
@end

@implementation MBIMBridge

+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    static __strong MBIMBridge *sharedBridge = nil;
    dispatch_once(&onceToken, ^{
        sharedBridge = [[MBIMBridge alloc] _init];
    });
    
    return sharedBridge;
}

- (instancetype)_init
{
    self = [super init];
    if (self) {
        self.port = kDefaultPort;
        
        _operationQueue = [[NSOperationQueue alloc] init];
        _operationQueue.maxConcurrentOperationCount = 5;
    }
    
    return self;
}

- (void)_terminate
{
    // *shrug*
    exit(1);
}

- (NSArray *)sslCertificateAndIdentity
{
    if (!_sslCertificateAndIdentity && self.sslCertPath) {
        // Get the p12
        NSError *error = nil;
        NSData *certData = [NSData dataWithContentsOfFile:self.sslCertPath options:0 error:&error];
        if (!certData || error) {
            MBIMLogError(@"Unable to load SSL certificate from file: %@", [error localizedDescription]);
            return nil;
        }
        
        CFArrayRef items = nil;
        OSStatus status = SecPKCS12Import(
            (__bridge CFDataRef)certData,
            (__bridge CFDictionaryRef) @{
                (__bridge id)kSecImportExportPassphrase : @"xNAq3vn)^PNu}[&gyQ4MZeV?J"
            },
            &items
        );
        
        if (status != noErr) {
            MBIMLogError(@"Error importing PKCS12: SecPKCS12Import status: %d", status);
            return nil;
        }
        
        CFDictionaryRef certDict = CFArrayGetValueAtIndex(items, 0);
        if (!certDict) {
            MBIMLogError(@"Error parsing the SSL certificate");
            return nil;
        }
        
        SecIdentityRef identity = (SecIdentityRef)CFDictionaryGetValue(certDict, kSecImportItemIdentity);
        _sslCertificateAndIdentity = @[ (__bridge id)identity ];
    }
    
    return _sslCertificateAndIdentity;
}

- (void)checkSSLCertificate
{
    if (self.usesSSL) {
        NSArray *certAndIdentity = [self sslCertificateAndIdentity];
        if ([certAndIdentity count]) {
            MBIMLogInfo(@"SSL Certificate looks okay");
        } else {
            MBIMLogFatal(@"Wasn't able to load SSL certificate. Bailing...");
            [self _terminate];
        }
    }
}

#pragma mark -
#pragma mark Connection

- (void)connect
{
#if HOOK_IMAGENT
    char *errorString = nil;
    BOOL hooked = HookIMAgent(self.dylibPath, &errorString);
    if (!hooked) {
        NSString *errorNSString = [NSString stringWithUTF8String:errorString];
        MBIMLogInfo(@"Error hooking imagent: %@", errorNSString);
        return;
    }
#endif
    
    [self registerForNotifications];
    
    [[IMDaemonController sharedInstance] setDelegate:(id)self];
    [[[IMDaemonController sharedInstance] listener] addHandler:self];
    
    if (![[IMDaemonController sharedInstance] hasListenerForID:MBIMBridgeToken]) {
        if (![[IMDaemonController sharedInstance] addListenerID:MBIMBridgeToken capabilities:(kFZListenerCapFileTransfers | kFZListenerCapManageStatus | kFZListenerCapChats | kFZListenerCapMessageHistory | kFZListenerCapIDQueries | kFZListenerCapSendMessages)]) {
            MBIMLogFatal(@"Failed to connect to imagent");
            
            [self _terminate];
        }
    }
    
    [self checkSSLCertificate];
    [self startWebServer];
}

- (void)disconnect
{
    [[IMDaemonController sharedInstance] removeListenerID:MBIMBridgeToken];
}

#pragma mark -
#pragma mark Notifications

- (void)registerForNotifications
{
    (void)[IMChatRegistry sharedInstance];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_messageReceived:) name:IMChatMessageReceivedNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_chatRegistryDidLoad:) name:IMChatRegistryDidLoadNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_chatItemsDidChange:) name:IMChatItemsDidChangeNotification object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_unreadCountChanged:) name:IMChatRegistryUnreadCountChangedNotification object:nil];
}

- (void)_unreadCountChanged:(NSNotification *)notification
{
    // Not a lot of useful information plumbed here...
}

- (void)_messageReceived:(NSNotification *)notification
{
    MBIMLogInfo(@"Received message from chat with GUID: %@", [[notification object] guid]);
    
    IMChat *chat = [notification object];
    IMMessage *message = [[notification userInfo] objectForKey:IMChatValueKey];
    if (chat && message) {
        if (![message isFromMe]) {
            MBIMUpdateItem *updateItem = [[MBIMUpdateItem alloc] init];
            updateItem.changedChat = chat;
            updateItem.addedMessage = message;
            
            [[MBIMUpdateQueue sharedInstance] enqueueUpdateItem:updateItem];
        } else {
            // TODO: care about messages from me? 
        }
    }
}

- (void)_chatRegistryDidLoad:(NSNotification *)notification
{
    MBIMLogInfo(@"Loaded chat registry. %lu existing chats", (unsigned long)[[IMChatRegistry sharedInstance] numberOfExistingChats]);
}

- (void)_chatItemsDidChange:(NSNotification *)notification
{
    IMChat *chat = [notification object];
    if (chat) {
        MBIMLogInfo(@"Chat items change for GUID: %@", [chat guid]);
        
        MBIMUpdateItem *updateItem = [[MBIMUpdateItem alloc] init];
        updateItem.changedChat = chat;
        [[MBIMUpdateQueue sharedInstance] enqueueUpdateItem:updateItem];
    }
}

#pragma mark -
#pragma mark Web Server initialization

- (void)startWebServer
{
    self.httpServer = [[MBIMConcurrentHTTPServer alloc] init];
    [self.httpServer setConnectionClass:[MBIMHTTPConnection class]];
    [self.httpServer setPort:self.port];
    
    NSError *error = nil;
    if (![self.httpServer start:&error]) {
        MBIMLogError(@"Error starting HTTP server: %@", [error localizedDescription]);
    } else {
        MBIMLogNotify(@"Started Kordophone HTTP server on port %u", self.port);
    }
}

#pragma mark -
#pragma mark Daemon lifecycle

- (void)daemonControllerWillConnect
{
    MBIMLogInfo(@"Connecting to imagent...");
}

- (void)daemonControllerDidConnect
{
    MBIMLogInfo(@"imagent responded.");
    
    IMAccount *iMessageAccount = [[IMAccountController sharedInstance] bestAccountForService:[IMServiceImpl iMessageService]];
    if (iMessageAccount) {
        MBIMLogInfo(@"Successfully got accounts from imagent");
        MBIMLogInfo(@"iMessage account connected: %@", iMessageAccount);
    } else {
        MBIMLogFatal(@"ERROR: imagent returned no accounts (not entitled? speak with Agent Hook)");
        [self _terminate];
    }
}

- (void)daemonControllerDidDisconnect
{
    MBIMLogInfo(@"Disconnected from imagent");
}

- (void)daemonConnectionLost
{
    MBIMLogError(@"Connection lost to imagent");
}

@end
