#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <Photos/Photos.h>
#import "photokit_bridge.h"

static volatile int photos_cancelled = 0;

static NSDictionary *make_error_dict(NSString *message) {
    return @{@"error": message};
}

static NSDictionary *collection_to_dict(PHCollection *collection) {
    NSString *name = collection.localizedTitle ?: @"Untitled";
    NSString *identifier = collection.localIdentifier ?: @"";

    if ([collection isKindOfClass:[PHCollectionList class]]) {
        PHCollectionList *list = (PHCollectionList *)collection;
        PHFetchResult<PHCollection *> *children = [PHCollectionList fetchCollectionsInCollectionList:list
                                                                                              options:nil];
        NSMutableArray *childList = [NSMutableArray arrayWithCapacity:children.count];
        for (NSUInteger i = 0; i < children.count; i++) {
            NSDictionary *child = collection_to_dict(children[i]);
            if (child) {
                [childList addObject:child];
            }
        }
        return @{
            @"id": identifier,
            @"name": name,
            @"kind": @"folder",
            @"children": childList,
        };
    }

    if ([collection isKindOfClass:[PHAssetCollection class]]) {
        return @{
            @"id": identifier,
            @"name": name,
            @"kind": @"album",
            @"children": @[],
        };
    }

    return nil;
}

static char *json_from_object(id obj) {
    if (!obj) return NULL;
    NSData *data = [NSJSONSerialization dataWithJSONObject:obj options:0 error:nil];
    if (!data) return NULL;
    const char *utf8 = [data bytes];
    size_t len = [data length];
    char *copy = (char *)malloc(len + 1);
    if (!copy) return NULL;
    memcpy(copy, utf8, len);
    copy[len] = '\0';
    return copy;
}

static NSData *nsimage_to_jpeg(NSImage *image, CGFloat compression) {
    NSData *result = nil;
    NSBitmapImageRep *rep = nil;
    for (NSImageRep *r in image.representations) {
        if ([r isKindOfClass:[NSBitmapImageRep class]]) {
            rep = (NSBitmapImageRep *)r;
            break;
        }
    }
    if (!rep) {
        rep = [[NSBitmapImageRep alloc] initWithData:[image TIFFRepresentation]];
    }
    if (rep) {
        NSDictionary *props = @{NSImageCompressionMethod: @(NSTIFFCompressionJPEG),
                                NSImageCompressionFactor: @(compression)};
        result = [rep representationUsingType:NSBitmapImageFileTypeJPEG properties:props];
    }
    return result;
}

static BOOL ensure_directory(NSString *outputDir) {
    NSFileManager *fm = [NSFileManager defaultManager];
    NSError *dirErr = nil;
    return [fm createDirectoryAtPath:outputDir withIntermediateDirectories:YES
                          attributes:nil error:&dirErr];
}

static PHFetchResult<PHAsset *> *fetch_assets_for_collection(PHAssetCollection *album) {
    PHFetchOptions *opts = [[PHFetchOptions alloc] init];
    opts.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate"
                                                           ascending:YES]];
    return [PHAsset fetchAssetsInAssetCollection:album options:opts];
}

static PHFetchResult<PHAsset *> *fetch_assets_for_album(const char *album_id) {
    if (!album_id) return nil;

    NSString *nsAlbumId = [NSString stringWithUTF8String:album_id];
    PHFetchResult<PHAssetCollection *> *albums =
        [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[nsAlbumId]
                                                             options:nil];
    if (albums.count == 0) return nil;

    return fetch_assets_for_collection(albums.firstObject);
}

static NSString *sanitized_asset_identifier(NSString *assetID) {
    NSMutableString *safe = [assetID mutableCopy];
    NSRange fullRange = NSMakeRange(0, safe.length);
    [safe replaceOccurrencesOfString:@"/"
                          withString:@"_"
                             options:0
                               range:fullRange];
    [safe replaceOccurrencesOfString:@"\\"
                          withString:@"_"
                             options:0
                               range:NSMakeRange(0, safe.length)];
    return safe;
}

static NSString *unique_path_for_filename(NSString *outputDir, NSString *filename, NSUInteger index) {
    NSFileManager *fm = [NSFileManager defaultManager];
    NSString *candidate = [outputDir stringByAppendingPathComponent:filename];
    if (![fm fileExistsAtPath:candidate]) {
        return candidate;
    }

    NSString *base = [filename stringByDeletingPathExtension];
    NSString *ext = [filename pathExtension];
    NSString *prefixed = ext.length > 0
        ? [NSString stringWithFormat:@"%04lu_%@.%@", (unsigned long)index, base, ext]
        : [NSString stringWithFormat:@"%04lu_%@", (unsigned long)index, base];
    return [outputDir stringByAppendingPathComponent:prefixed];
}

static NSString *sanitized_path_component(NSString *name) {
    NSString *source = name ?: @"Untitled";
    NSMutableString *safe = [[source stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy];
    if (safe.length == 0) {
        return @"Untitled";
    }
    NSRange fullRange = NSMakeRange(0, safe.length);
    [safe replaceOccurrencesOfString:@"/"
                          withString:@"_"
                             options:0
                               range:fullRange];
    [safe replaceOccurrencesOfString:@"\\"
                          withString:@"_"
                             options:0
                               range:NSMakeRange(0, safe.length)];
    return safe;
}

static int export_preview_assets(PHFetchResult<PHAsset *> *assets, NSString *outputDir, int target_size) {
    PHImageManager *im = [PHImageManager defaultManager];
    CGFloat scale = (CGFloat)target_size;
    CGSize targetCGSize = CGSizeMake(scale, scale);
    PHImageRequestOptions *imgOpts = [[PHImageRequestOptions alloc] init];
    imgOpts.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
    imgOpts.resizeMode = PHImageRequestOptionsResizeModeExact;
    imgOpts.networkAccessAllowed = YES;
    imgOpts.synchronous = YES;

    __block int exported = 0;
    __block int failed = 0;

    for (NSUInteger i = 0; i < assets.count; i++) {
        if (photos_cancelled) break;
        PHAsset *asset = assets[i];
        dispatch_semaphore_t sem = dispatch_semaphore_create(0);
        __block NSData *imageData = nil;

        [im requestImageForAsset:asset
                       targetSize:targetCGSize
                      contentMode:PHImageContentModeAspectFit
                          options:imgOpts
                    resultHandler:^(NSImage *result, NSDictionary *info) {
            if (info[PHImageErrorKey]) {
                dispatch_semaphore_signal(sem);
                return;
            }
            if (result) {
                imageData = nsimage_to_jpeg(result, 0.85);
            }
            dispatch_semaphore_signal(sem);
        }];

        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

        if (!imageData) {
            failed++;
            continue;
        }

        NSString *safe = sanitized_asset_identifier(asset.localIdentifier);
        NSString *filename = [NSString stringWithFormat:@"%04lu_%@.jpg", (unsigned long)i, safe];
        NSString *filepath = [outputDir stringByAppendingPathComponent:filename];

        NSError *writeErr = nil;
        if (![imageData writeToFile:filepath options:NSDataWritingAtomic error:&writeErr]) {
            failed++;
            continue;
        }

        exported++;
    }

    return (failed > 0 && exported == 0) ? -4 : exported;
}

static int export_original_assets(PHFetchResult<PHAsset *> *assets, NSString *outputDir) {
    PHAssetResourceManager *rm = [PHAssetResourceManager defaultManager];
    PHAssetResourceRequestOptions *opts = [[PHAssetResourceRequestOptions alloc] init];
    opts.networkAccessAllowed = YES;

    __block int exported = 0;
    __block int failed = 0;

    for (NSUInteger i = 0; i < assets.count; i++) {
        if (photos_cancelled) break;
        PHAsset *asset = assets[i];
        NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
        if (resources.count == 0) {
            failed++;
            continue;
        }

        PHAssetResource *resource = resources.firstObject;
        NSString *filename = resource.originalFilename;
        if (!filename || filename.length == 0) {
            filename = sanitized_asset_identifier(asset.localIdentifier);
        }

        NSString *filepath = unique_path_for_filename(outputDir, filename, i);
        NSURL *fileURL = [NSURL fileURLWithPath:filepath];

        dispatch_semaphore_t sem = dispatch_semaphore_create(0);
        __block NSError *writeErr = nil;
        [rm writeDataForAssetResource:resource
                              toFile:fileURL
                             options:opts
                   completionHandler:^(NSError * _Nullable error) {
            writeErr = error;
            dispatch_semaphore_signal(sem);
        }];
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

        if (writeErr) {
            failed++;
            continue;
        }
        exported++;
    }

    return (failed > 0 && exported == 0) ? -4 : exported;
}

static int backup_collection(PHCollection *collection, NSString *outputDir, int target_size, BOOL originals) {
    NSString *path = [outputDir stringByAppendingPathComponent:sanitized_path_component(collection.localizedTitle)];

    if ([collection isKindOfClass:[PHCollectionList class]]) {
        PHCollectionList *list = (PHCollectionList *)collection;
        PHFetchResult<PHCollection *> *children = [PHCollectionList fetchCollectionsInCollectionList:list options:nil];
        __block int total = 0;
        __block BOOL failedAny = NO;
        for (NSUInteger i = 0; i < children.count; i++) {
            if (photos_cancelled) break;
            int rc = backup_collection(children[i], path, target_size, originals);
            if (rc >= 0) {
                total += rc;
            } else if (rc == -4) {
                failedAny = YES;
            } else {
                return rc;
            }
        }
        if (failedAny && total == 0) {
            return -4;
        }
        return total;
    }

    if ([collection isKindOfClass:[PHAssetCollection class]]) {
        if (!ensure_directory(path)) {
            return -2;
        }
        PHFetchResult<PHAsset *> *assets = fetch_assets_for_collection((PHAssetCollection *)collection);
        return originals ? export_original_assets(assets, path) : export_preview_assets(assets, path, target_size);
    }

    return 0;
}

int photos_request_access(void) {
    __block PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
    if (status == PHAuthorizationStatusNotDetermined) {
        dispatch_semaphore_t sem = dispatch_semaphore_create(0);
        [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus s) {
            status = s;
            dispatch_semaphore_signal(sem);
        }];
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    }
    return (status == PHAuthorizationStatusAuthorized) ? 0 : -1;
}

char *photos_list_albums_json(void) {
    PHFetchResult<PHAssetCollection *> *albums =
        [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum
                                                 subtype:PHAssetCollectionSubtypeAny
                                                 options:nil];

    NSMutableArray *list = [NSMutableArray arrayWithCapacity:albums.count];
    for (NSUInteger i = 0; i < albums.count; i++) {
        PHAssetCollection *album = albums[i];
        NSString *title = album.localizedTitle;
        NSString *lid = album.localIdentifier;

        [list addObject:@{
            @"id": lid ?: @"",
            @"title": title ?: @""
        }];
    }

    return json_from_object(@{@"albums": list});
}

static NSString *asset_cloud_status_string(PHAsset *asset) {
    @try {
        id cloudId = [asset performSelector:@selector(cloudIdentifier)];
        if (cloudId && ![cloudId isEqual:[NSNull null]]) {
            return @"cloud";
        }
    } @catch (NSException *exception) {
    }
    PHAssetResource *resource = [PHAssetResource assetResourcesForAsset:asset].firstObject;
    if (resource) {
        @try {
            id locallyAvailable = [resource performSelector:@selector(isLocallyAvailable)];
            if (locallyAvailable && [locallyAvailable boolValue]) {
                return @"local";
            }
            return @"cloud";
        } @catch (NSException *exception) {
        }
    }
    return @"local";
}

char *photos_list_assets_json(const char *album_id) {
    if (!album_id) return json_from_object(make_error_dict(@"album_id is required"));

    NSString *nsAlbumId = [NSString stringWithUTF8String:album_id];
    PHFetchResult<PHAssetCollection *> *albums =
        [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[nsAlbumId]
                                                             options:nil];
    if (albums.count == 0) return json_from_object(make_error_dict(@"album not found"));

    PHAssetCollection *album = albums.firstObject;
    PHFetchOptions *opts = [[PHFetchOptions alloc] init];
    opts.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate"
                                                            ascending:YES]];
    PHFetchResult<PHAsset *> *assets =
        [PHAsset fetchAssetsInAssetCollection:album options:opts];

    NSMutableArray *list = [NSMutableArray arrayWithCapacity:assets.count];
    for (NSUInteger i = 0; i < assets.count; i++) {
        PHAsset *asset = assets[i];
        NSString *lid = asset.localIdentifier;
        NSString *filename = nil;
        NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
        if (resources.count > 0) {
            filename = resources.firstObject.originalFilename;
        }
        NSString *cloudStatus = asset_cloud_status_string(asset);
        [list addObject:@{
            @"id": lid ?: @"",
            @"filename": filename ?: @"",
            @"cloud": cloudStatus
        }];
    }

    return json_from_object(@{@"assets": list, @"total": @(assets.count)});
}

char *photos_list_tree_json(void) {
    PHFetchResult<PHCollection *> *collections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];

    NSMutableArray *list = [NSMutableArray arrayWithCapacity:collections.count];
    for (NSUInteger i = 0; i < collections.count; i++) {
        NSDictionary *node = collection_to_dict(collections[i]);
        if (node) {
            [list addObject:node];
        }
    }

    return json_from_object(@{@"collections": list});
}

int photos_export_album_previews(const char *album_id, const char *output_dir, int target_size) {
    if (!album_id || !output_dir) return -1;
    if (target_size <= 0) target_size = 1024;

    NSString *nsOutputDir = [NSString stringWithUTF8String:output_dir];

    if (!ensure_directory(nsOutputDir)) {
        return -2;
    }

    PHFetchResult<PHAsset *> *assets = fetch_assets_for_album(album_id);
    if (!assets) return -3;

    return export_preview_assets(assets, nsOutputDir, target_size);
}

int photos_export_album_originals(const char *album_id, const char *output_dir) {
    if (!album_id || !output_dir) return -1;

    NSString *nsOutputDir = [NSString stringWithUTF8String:output_dir];
    if (!ensure_directory(nsOutputDir)) {
        return -2;
    }

    PHFetchResult<PHAsset *> *assets = fetch_assets_for_album(album_id);
    if (!assets) return -3;

    return export_original_assets(assets, nsOutputDir);
}

int photos_backup_all(const char *output_dir, int target_size, int originals) {
    if (!output_dir) return -1;
    if (!originals && target_size <= 0) target_size = 1024;

    NSString *nsOutputDir = [NSString stringWithUTF8String:output_dir];
    if (!ensure_directory(nsOutputDir)) {
        return -2;
    }

    PHFetchResult<PHCollection *> *collections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
    __block int total = 0;
    __block BOOL failedAny = NO;

    for (NSUInteger i = 0; i < collections.count; i++) {
        if (photos_cancelled) break;
        int rc = backup_collection(collections[i], nsOutputDir, target_size, originals != 0);
        if (rc >= 0) {
            total += rc;
        } else if (rc == -4) {
            failedAny = YES;
        } else {
            return rc;
        }
    }

    if (failedAny && total == 0) {
        return -4;
    }
    return total;
}

char *photos_export_preview_json(const char *asset_id, const char *output_dir, int target_size, int index) {
    if (!asset_id || !output_dir) return json_from_object(make_error_dict(@"asset_id and output_dir required"));
    if (target_size <= 0) target_size = 1024;

    NSString *nsAssetId = [NSString stringWithUTF8String:asset_id];
    NSString *nsOutputDir = [NSString stringWithUTF8String:output_dir];

    PHFetchResult<PHAsset *> *fetch = [PHAsset fetchAssetsWithLocalIdentifiers:@[nsAssetId] options:nil];
    if (fetch.count == 0) return json_from_object(make_error_dict(@"asset not found"));

    PHAsset *asset = fetch.firstObject;

    PHImageManager *im = [PHImageManager defaultManager];
    CGFloat scale = (CGFloat)target_size;
    CGSize targetCGSize = CGSizeMake(scale, scale);
    PHImageRequestOptions *imgOpts = [[PHImageRequestOptions alloc] init];
    imgOpts.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
    imgOpts.resizeMode = PHImageRequestOptionsResizeModeExact;
    imgOpts.networkAccessAllowed = YES;
    imgOpts.synchronous = YES;

    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    __block NSData *imageData = nil;
    __block NSDictionary *errorInfo = nil;

    [im requestImageForAsset:asset
                   targetSize:targetCGSize
                  contentMode:PHImageContentModeAspectFit
                      options:imgOpts
                resultHandler:^(NSImage *result, NSDictionary *info) {
        if (info[PHImageErrorKey]) {
            errorInfo = info;
            dispatch_semaphore_signal(sem);
            return;
        }
        if (result) {
            imageData = nsimage_to_jpeg(result, 0.85);
        }
        dispatch_semaphore_signal(sem);
    }];

    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

    if (!imageData) {
        return json_from_object(@{@"error": @"failed to render image", @"cloud": asset_cloud_status_string(asset)});
    }

    NSString *safe = sanitized_asset_identifier(asset.localIdentifier);
    NSString *filename = [NSString stringWithFormat:@"%04lu_%@.jpg", (unsigned long)index, safe];
    NSString *filepath = [nsOutputDir stringByAppendingPathComponent:filename];

    NSError *writeErr = nil;
    if (![imageData writeToFile:filepath options:NSDataWritingAtomic error:&writeErr]) {
        return json_from_object(@{@"error": @"write failed", @"cloud": asset_cloud_status_string(asset)});
    }

    NSNumber *fileSize = nil;
    NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filepath error:nil];
    if (attrs) {
        fileSize = attrs[NSFileSize];
    }

    return json_from_object(@{
        @"filename": filename,
        @"size": fileSize ?: @0,
        @"cloud": asset_cloud_status_string(asset)
    });
}

char *photos_export_original_json(const char *asset_id, const char *output_dir, int index) {
    if (!asset_id || !output_dir) return json_from_object(make_error_dict(@"asset_id and output_dir required"));

    NSString *nsAssetId = [NSString stringWithUTF8String:asset_id];
    NSString *nsOutputDir = [NSString stringWithUTF8String:output_dir];

    PHFetchResult<PHAsset *> *fetch = [PHAsset fetchAssetsWithLocalIdentifiers:@[nsAssetId] options:nil];
    if (fetch.count == 0) return json_from_object(make_error_dict(@"asset not found"));

    PHAsset *asset = fetch.firstObject;

    NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
    if (resources.count == 0) {
        return json_from_object(@{@"error": @"no resources", @"cloud": asset_cloud_status_string(asset)});
    }

    PHAssetResource *resource = resources.firstObject;
    NSString *filename = resource.originalFilename;
    if (!filename || filename.length == 0) {
        filename = sanitized_asset_identifier(asset.localIdentifier);
    }

    NSString *filepath = unique_path_for_filename(nsOutputDir, filename, (NSUInteger)index);
    NSURL *fileURL = [NSURL fileURLWithPath:filepath];

    PHAssetResourceManager *rm = [PHAssetResourceManager defaultManager];
    PHAssetResourceRequestOptions *opts = [[PHAssetResourceRequestOptions alloc] init];
    opts.networkAccessAllowed = YES;

    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    __block NSError *writeErr = nil;
    [rm writeDataForAssetResource:resource
                          toFile:fileURL
                         options:opts
               completionHandler:^(NSError * _Nullable error) {
        writeErr = error;
        dispatch_semaphore_signal(sem);
    }];
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

    if (writeErr) {
        return json_from_object(@{@"error": @"write failed", @"cloud": asset_cloud_status_string(asset)});
    }

    NSString *writtenFilename = [filepath lastPathComponent];
    NSNumber *fileSize = nil;
    NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filepath error:nil];
    if (attrs) {
        fileSize = attrs[NSFileSize];
    }

    return json_from_object(@{
        @"filename": writtenFilename,
        @"size": fileSize ?: @0,
        @"cloud": asset_cloud_status_string(asset)
    });
}

void photos_free_string(char *value) {
    if (value) free(value);
}

void photos_request_cancel(void) {
    photos_cancelled = 1;
}
