// Copyright (C) 2026, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

#import "DuplicatiPhotos.h"

#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#import <Photos/Photos.h>
#include <math.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>

// Static variable to control logging, initialized from environment variable
static int g_loggingEnabled = -1;
static pthread_mutex_t g_loggingMutex = PTHREAD_MUTEX_INITIALIZER;

static void DuplicatiPhotosInitializeLogging(void) {
    if (g_loggingEnabled == -1) {
        g_loggingEnabled = getenv("DEBUG_PHOTOKIT") ? 1 : 0;
    }
}

#define DLog(...) do { \
    if (g_loggingEnabled == -1) DuplicatiPhotosInitializeLogging(); \
    if (g_loggingEnabled) { \
        pthread_mutex_lock(&g_loggingMutex); \
        NSLog(__VA_ARGS__); \
        pthread_mutex_unlock(&g_loggingMutex); \
    } \
} while(0)

static char *DuplicatiPhotosCopyCString(NSString *value) {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosCopyCString called with value: %@", value ?: @"(null)");
    if (!value) {
        return NULL;
    }

    const char *utf8 = [value cStringUsingEncoding:NSUTF8StringEncoding];
    if (!utf8) {
        return NULL;
    }

    size_t length = strlen(utf8);
    char *copy = malloc(length + 1);
    if (!copy) {
        DLog(@"DuplicatiPhotos: DuplicatiPhotosCopyCString failed to allocate memory");
        return NULL;
    }

    memcpy(copy, utf8, length);
    copy[length] = '\0';
    DLog(@"DuplicatiPhotos: DuplicatiPhotosCopyCString returning copy of length %zu", length);
    return copy;
}

static char *DuplicatiPhotosCopyError(const char *message) {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosCopyError called with message: %s", message ?: "(null)");
    if (!message) {
        return NULL;
    }

    size_t length = strlen(message);
    char *copy = malloc(length + 1);
    if (!copy) {
        DLog(@"DuplicatiPhotos: DuplicatiPhotosCopyError failed to allocate memory");
        return NULL;
    }

    memcpy(copy, message, length);
    copy[length] = '\0';
    DLog(@"DuplicatiPhotos: DuplicatiPhotosCopyError returning copy");
    return copy;
}

static char *DuplicatiPhotosCopyErrorFromString(NSString *value) {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosCopyErrorFromString called with value: %@", value ?: @"(null)");
    if (!value) {
        return NULL;
    }

    return DuplicatiPhotosCopyCString(value);
}

static PHAssetResource *DuplicatiPhotosSelectResource(PHAsset *asset) {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosSelectResource called for asset: %@", asset.localIdentifier);
    NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
    DLog(@"DuplicatiPhotos: Found %lu resources for asset", (unsigned long)resources.count);
    if (resources.count == 0) {
        return nil;
    }

    PHAssetResource *selected = resources.firstObject;
    for (PHAssetResource *candidate in resources) {
        PHAssetResourceType type = candidate.type;
        DLog(@"DuplicatiPhotos: Evaluating resource type: %ld", (long)type);
        if (type == PHAssetResourceTypeFullSizePhoto ||
            type == PHAssetResourceTypePhoto ||
            type == PHAssetResourceTypeFullSizePairedVideo ||
            type == PHAssetResourceTypeVideo ||
            type == PHAssetResourceTypeAudio) {
            selected = candidate;
            DLog(@"DuplicatiPhotos: Selected resource type: %ld", (long)type);
            break;
        }
    }

    DLog(@"DuplicatiPhotos: Returning selected resource: %@", selected.originalFilename);
    return selected;
}

int DuplicatiPhotosEnumerateAssets(DuplicatiPhotosAssetMetadata **assetsOut, size_t *countOut, char **errorMessageOut) {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosEnumerateAssets called");
    if (!assetsOut || !countOut) {
        DLog(@"DuplicatiPhotos: Invalid arguments to DuplicatiPhotosEnumerateAssets");
        if (errorMessageOut) {
            *errorMessageOut = DuplicatiPhotosCopyError("invalid arguments");
        }
        return -1;
    }

    *assetsOut = NULL;
    *countOut = 0;
    if (errorMessageOut) {
        *errorMessageOut = NULL;
    }

    @autoreleasepool {
        PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
        DLog(@"DuplicatiPhotos: Authorization status: %ld", (long)status);
        if (status == PHAuthorizationStatusDenied || status == PHAuthorizationStatusRestricted) {
            if (errorMessageOut) {
                *errorMessageOut = DuplicatiPhotosCopyError("Photos access denied or restricted");
            }
            return -1;
        } else if (status == PHAuthorizationStatusNotDetermined) {
            DLog(@"DuplicatiPhotos: Requesting authorization");
            // Request authorization synchronously (blocking)
            __block PHAuthorizationStatus newStatus = PHAuthorizationStatusNotDetermined;
            dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
            [PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite handler:^(PHAuthorizationStatus authStatus) {
                newStatus = authStatus;
                dispatch_semaphore_signal(semaphore);
            }];
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            DLog(@"DuplicatiPhotos: Authorization result: %ld", (long)newStatus);
            if (newStatus != PHAuthorizationStatusAuthorized) {
                if (errorMessageOut) {
                    *errorMessageOut = DuplicatiPhotosCopyError("Photos access not granted, go to System Settings to allow access");
                }
                return -1;
            }
        }

        PHFetchOptions *options = [[PHFetchOptions alloc] init];
        PHFetchResult<PHAsset *> *assets = [PHAsset fetchAssetsWithOptions:options];
        DLog(@"DuplicatiPhotos: Fetched %lu assets", (unsigned long)assets.count);
        if (assets.count == 0) {
            return 0;
        }

        NSMutableArray<NSDictionary *> *results = [NSMutableArray arrayWithCapacity:assets.count];
        for (NSUInteger idx = 0; idx < assets.count; idx++) {
            PHAsset *asset = [assets objectAtIndex:idx];
            PHAssetResource *resource = DuplicatiPhotosSelectResource(asset);
            if (!resource) {
                DLog(@"DuplicatiPhotos: Asset %lu has no valid resource", (unsigned long)idx);
                continue;
            }

            NSString *identifier = asset.localIdentifier ?: @"";
            NSString *filename = resource.originalFilename ?: @"";
            NSString *uti = resource.uniformTypeIdentifier;
            NSNumber *sizeValue = [resource valueForKey:@"fileSize"];
            NSNumber *creation = asset.creationDate ? @([asset.creationDate timeIntervalSince1970]) : nil;
            NSNumber *modification = asset.modificationDate ? @([asset.modificationDate timeIntervalSince1970]) : nil;

            NSDictionary *entry = @{ @"identifier": identifier,
                                     @"filename": filename,
                                     @"uti": uti ?: (id)[NSNull null],
                                     @"size": sizeValue ?: (id)[NSNull null],
                                     @"mediaType": @(asset.mediaType),
                                     @"pixelWidth": @(asset.pixelWidth),
                                     @"pixelHeight": @(asset.pixelHeight),
                                     @"creation": creation ?: (id)[NSNull null],
                                     @"modification": modification ?: (id)[NSNull null] };
            [results addObject:entry];
        }

        DLog(@"DuplicatiPhotos: After filtering, %lu assets with resources", (unsigned long)results.count);
        if (results.count == 0) {
            return 0;
        }

        DLog(@"DuplicatiPhotos: Allocating buffer for %lu assets, size per asset: %zu", (unsigned long)results.count, sizeof(DuplicatiPhotosAssetMetadata));
        DuplicatiPhotosAssetMetadata *buffer = calloc(results.count, sizeof(DuplicatiPhotosAssetMetadata));
        if (!buffer) {
            DLog(@"DuplicatiPhotos: Failed to allocate buffer");
            if (errorMessageOut) {
                *errorMessageOut = DuplicatiPhotosCopyError("out of memory");
            }
            return -1;
        }
        DLog(@"DuplicatiPhotos: Buffer allocated successfully at %p", buffer);

        for (NSUInteger idx = 0; idx < results.count; idx++) {
            DLog(@"DuplicatiPhotos: Processing asset %lu of %lu", (unsigned long)idx, (unsigned long)results.count);
            NSDictionary *entry = results[idx];
            
            DLog(@"DuplicatiPhotos: Copying identifier for asset %lu", (unsigned long)idx);
            buffer[idx].identifier = DuplicatiPhotosCopyCString(entry[@"identifier"]);
            
            DLog(@"DuplicatiPhotos: Copying filename for asset %lu", (unsigned long)idx);
            buffer[idx].filename = DuplicatiPhotosCopyCString(entry[@"filename"]);
            
            DLog(@"DuplicatiPhotos: Processing UTI for asset %lu", (unsigned long)idx);
            id utiValue = entry[@"uti"];
            buffer[idx].uti = (utiValue && utiValue != [NSNull null]) ? DuplicatiPhotosCopyCString(utiValue) : NULL;
            
            DLog(@"DuplicatiPhotos: Processing size for asset %lu", (unsigned long)idx);
            id sizeValue = entry[@"size"];
            buffer[idx].size = (sizeValue && sizeValue != [NSNull null]) ? [sizeValue longLongValue] : -1;
            
            DLog(@"DuplicatiPhotos: Processing mediaType for asset %lu", (unsigned long)idx);
            buffer[idx].mediaType = [entry[@"mediaType"] intValue];
            
            DLog(@"DuplicatiPhotos: Processing dimensions for asset %lu", (unsigned long)idx);
            buffer[idx].pixelWidth = [entry[@"pixelWidth"] intValue];
            buffer[idx].pixelHeight = [entry[@"pixelHeight"] intValue];
            
            DLog(@"DuplicatiPhotos: Processing creation date for asset %lu", (unsigned long)idx);
            id creation = entry[@"creation"];
            buffer[idx].creationDateSeconds = (creation && creation != [NSNull null]) ? [creation doubleValue] : NAN;
            
            DLog(@"DuplicatiPhotos: Processing modification date for asset %lu", (unsigned long)idx);
            id modification = entry[@"modification"];
            buffer[idx].modificationDateSeconds = (modification && modification != [NSNull null]) ? [modification doubleValue] : NAN;
            
            DLog(@"DuplicatiPhotos: Completed processing asset %lu", (unsigned long)idx);
        }

        DLog(@"DuplicatiPhotos: All assets processed, setting output parameters");
        *assetsOut = buffer;
        *countOut = results.count;
        DLog(@"DuplicatiPhotos: Returning success with %lu assets", (unsigned long)results.count);
        return 0;
    }
}

void DuplicatiPhotosFreeAssets(DuplicatiPhotosAssetMetadata *assets, size_t count) {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosFreeAssets called with count: %zu", count);
    if (!assets) {
        DLog(@"DuplicatiPhotos: DuplicatiPhotosFreeAssets called with NULL assets");
        return;
    }

    for (size_t idx = 0; idx < count; idx++) {
        if (assets[idx].identifier) {
            free(assets[idx].identifier);
        }
        if (assets[idx].filename) {
            free(assets[idx].filename);
        }
        if (assets[idx].uti) {
            free(assets[idx].uti);
        }
    }

    free(assets);
}

@interface DuplicatiPhotosReader : NSObject
@property (nonatomic, strong) NSMutableData *buffer;
@property (nonatomic, strong) NSCondition *condition;
@property (nonatomic, assign) BOOL completed;
@property (nonatomic, assign) BOOL cancelled;
@property (nonatomic, strong) NSError *error;
@property (nonatomic, assign) PHAssetResourceDataRequestID requestId;
@property (nonatomic, strong) PHAssetResource *resource;
@end

@implementation DuplicatiPhotosReader

- (instancetype)initWithResource:(PHAssetResource *)resource {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosReader initWithResource called for: %@", resource.originalFilename);
    self = [super init];
    if (self) {
        _resource = resource;
        _buffer = [NSMutableData data];
        _condition = [[NSCondition alloc] init];
        _completed = NO;
        _cancelled = NO;
        _requestId = PHInvalidAssetResourceDataRequestID;

        PHAssetResourceRequestOptions *options = [[PHAssetResourceRequestOptions alloc] init];
        options.networkAccessAllowed = YES;

        DLog(@"DuplicatiPhotos: Starting data request for resource: %@", resource.originalFilename);
        __weak typeof(self) weakSelf = self;
        PHAssetResourceManager *manager = [PHAssetResourceManager defaultManager];
        _requestId = [manager requestDataForAssetResource:resource
                                                 options:options
                                     dataReceivedHandler:^(NSData * _Nonnull data) {
            @autoreleasepool {
                DLog(@"DuplicatiPhotos: Data received handler called on thread %@", [NSThread currentThread]);
                __strong typeof(weakSelf) strongSelf = weakSelf;
                if (!strongSelf || !data || strongSelf.cancelled) {
                    DLog(@"DuplicatiPhotos: Data received handler skipped (cancelled or invalid)");
                    return;
                }

                DLog(@"DuplicatiPhotos: Received data chunk of size: %lu", (unsigned long)data.length);
                [strongSelf.condition lock];
                DLog(@"DuplicatiPhotos: Locked condition, current buffer size: %lu, appending data", (unsigned long)strongSelf.buffer.length);
                [strongSelf.buffer appendData:data];
                DLog(@"DuplicatiPhotos: Data appended, new buffer size: %lu", (unsigned long)strongSelf.buffer.length);
                [strongSelf.condition signal];
                DLog(@"DuplicatiPhotos: Signaled condition, unlocking");
                [strongSelf.condition unlock];
                DLog(@"DuplicatiPhotos: Data received handler completed");
            }
        }
        completionHandler:^(NSError * _Nullable error) {
            @autoreleasepool {
                __strong typeof(weakSelf) strongSelf = weakSelf;
                if (!strongSelf)
                    return;

                if (error) {
                    DLog(@"DuplicatiPhotos: Completion handler called with error: %@", error.localizedDescription);
                } else {
                    DLog(@"DuplicatiPhotos: Completion handler called successfully");
                }
                [strongSelf.condition lock];
                strongSelf.completed = YES;
                strongSelf.error = error;
                [strongSelf.condition broadcast];
                [strongSelf.condition unlock];
            }
        }];
    }
    return self;
}

- (void)dealloc {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosReader dealloc called");
    [self close];
}

- (void)close {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosReader close called");
    if (self.cancelled) {
        DLog(@"DuplicatiPhotos: Already cancelled, skipping close");
        return;
    }

    self.cancelled = YES;
    if (self.requestId != PHInvalidAssetResourceDataRequestID)
    {
        PHAssetResourceManager *manager = [PHAssetResourceManager defaultManager];
        [manager cancelDataRequest:self.requestId];
    }

    [self.condition lock];
    self.completed = YES;
    [self.condition broadcast];
    [self.condition unlock];
}

- (ssize_t)readInto:(uint8_t *)destination length:(size_t)length error:(char **)errorMessageOut {
    DLog(@"DuplicatiPhotos: readInto called with length: %zu", length);
    if (!destination) {
        DLog(@"DuplicatiPhotos: Invalid destination buffer");
        if (errorMessageOut) {
            *errorMessageOut = DuplicatiPhotosCopyError("invalid buffer");
        }
        return -1;
    }

    if (length == 0) {
        DLog(@"DuplicatiPhotos: Zero length read requested");
        return 0;
    }

    DLog(@"DuplicatiPhotos: Locking condition for read on thread %@", [NSThread currentThread]);
    [self.condition lock];
    DLog(@"DuplicatiPhotos: Condition locked, buffer length: %lu, completed: %d", (unsigned long)self.buffer.length, self.completed);
    while (self.buffer.length == 0 && !self.completed) {
        DLog(@"DuplicatiPhotos: Waiting for data...");
        [self.condition wait];
        DLog(@"DuplicatiPhotos: Woke up from wait, buffer length: %lu, completed: %d", (unsigned long)self.buffer.length, self.completed);
    }

    if (self.buffer.length == 0) {
        DLog(@"DuplicatiPhotos: Buffer is empty after wait");
        NSError *error = self.error;
        [self.condition unlock];

        if (error) {
            DLog(@"DuplicatiPhotos: Returning error: %@", error.localizedDescription);
            if (errorMessageOut) {
                *errorMessageOut = DuplicatiPhotosCopyErrorFromString(error.localizedDescription ?: @"read error");
            }
            return -1;
        }

        DLog(@"DuplicatiPhotos: No error, returning EOF");
        return 0;
    }

    size_t toCopy = MIN((size_t)self.buffer.length, length);
    DLog(@"DuplicatiPhotos: Copying %zu bytes to destination from buffer of size %lu", toCopy, (unsigned long)self.buffer.length);
    memcpy(destination, self.buffer.bytes, toCopy);
    DLog(@"DuplicatiPhotos: memcpy completed, removing copied bytes from buffer");
    [self.buffer replaceBytesInRange:NSMakeRange(0, toCopy) withBytes:NULL length:0];
    DLog(@"DuplicatiPhotos: Buffer updated, new size: %lu, unlocking", (unsigned long)self.buffer.length);
    [self.condition unlock];
    DLog(@"DuplicatiPhotos: Returning %zd bytes", (ssize_t)toCopy);
    return (ssize_t)toCopy;
}

@end

int DuplicatiPhotosOpenAsset(const char *identifier, void **handleOut, char **errorMessageOut) {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosOpenAsset called with identifier: %s", identifier ?: "(null)");
    if (!identifier || !handleOut) {
        DLog(@"DuplicatiPhotos: Invalid arguments to DuplicatiPhotosOpenAsset");
        if (errorMessageOut) {
            *errorMessageOut = DuplicatiPhotosCopyError("invalid arguments");
        }
        return -1;
    }

    *handleOut = NULL;
    if (errorMessageOut) {
        *errorMessageOut = NULL;
    }

    @autoreleasepool {
        NSString *identifierString = [NSString stringWithUTF8String:identifier];
        if (!identifierString) {
            if (errorMessageOut) {
                *errorMessageOut = DuplicatiPhotosCopyError("invalid identifier");
            }
            return -1;
        }

        PHFetchResult<PHAsset *> *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[identifierString] options:nil];
        if (fetchResult.count == 0) {
            if (errorMessageOut) {
                *errorMessageOut = DuplicatiPhotosCopyError("asset not found");
            }
            return -1;
        }

        PHAsset *asset = [fetchResult objectAtIndex:0];
        PHAssetResource *resource = DuplicatiPhotosSelectResource(asset);
        if (!resource) {
            if (errorMessageOut) {
                *errorMessageOut = DuplicatiPhotosCopyError("asset resource unavailable");
            }
            return -1;
        }

        DuplicatiPhotosReader *reader = [[DuplicatiPhotosReader alloc] initWithResource:resource];
        if (!reader) {
            if (errorMessageOut) {
                *errorMessageOut = DuplicatiPhotosCopyError("unable to create reader");
            }
            return -1;
        }

        *handleOut = (void *)CFBridgingRetain(reader);
        return 0;
    }
}

ssize_t DuplicatiPhotosReadAsset(void *handle, uint8_t *buffer, size_t bufferLength, char **errorMessageOut) {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosReadAsset called with bufferLength: %zu", bufferLength);
    if (!handle) {
        DLog(@"DuplicatiPhotos: Invalid handle to DuplicatiPhotosReadAsset");
        if (errorMessageOut) {
            *errorMessageOut = DuplicatiPhotosCopyError("invalid handle");
        }
        return -1;
    }

    DuplicatiPhotosReader *reader = (__bridge DuplicatiPhotosReader *)handle;
    ssize_t result = [reader readInto:buffer length:bufferLength error:errorMessageOut];
    DLog(@"DuplicatiPhotos: DuplicatiPhotosReadAsset returning: %zd", result);
    return result;
}

int DuplicatiPhotosGetAssetSize(void *handle, int64_t *sizeOut, char **errorMessageOut) {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosGetAssetSize called");
    if (!handle || !sizeOut) {
        DLog(@"DuplicatiPhotos: Invalid arguments to DuplicatiPhotosGetAssetSize");
        if (errorMessageOut) {
            *errorMessageOut = DuplicatiPhotosCopyError("invalid arguments");
        }
        return -1;
    }

    if (errorMessageOut) {
        *errorMessageOut = NULL;
    }

    @autoreleasepool {
        DuplicatiPhotosReader *reader = (__bridge DuplicatiPhotosReader *)handle;
        PHAssetResource *resource = reader.resource;
        
        if (!resource) {
            if (errorMessageOut) {
                *errorMessageOut = DuplicatiPhotosCopyError("resource not available");
            }
            return -1;
        }

        NSNumber *sizeValue = [resource valueForKey:@"fileSize"];
        if (!sizeValue) {
            if (errorMessageOut) {
                *errorMessageOut = DuplicatiPhotosCopyError("size not available");
            }
            return -1;
        }

        *sizeOut = [sizeValue longLongValue];
        DLog(@"DuplicatiPhotos: DuplicatiPhotosGetAssetSize returning size: %lld", *sizeOut);
        return 0;
    }
}

void DuplicatiPhotosCloseAsset(void *handle) {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosCloseAsset called");
    if (!handle) {
        DLog(@"DuplicatiPhotos: DuplicatiPhotosCloseAsset called with NULL handle");
        return;
    }

    DuplicatiPhotosReader *reader = (__bridge_transfer DuplicatiPhotosReader *)handle;
    [reader close];
}

void DuplicatiPhotosFreeString(char *value) {
    DLog(@"DuplicatiPhotos: DuplicatiPhotosFreeString called");
    if (value) {
        free(value);
    }
}
