#! /usr/bin/env python
import os
import requests
import argparse
import errno
from datetime import datetime
from functools import lru_cache

from fuse import FUSE, Operations

class ImmichFuse(Operations):
    def __init__(self, api_key, base_url):
        self.api_key = api_key
        self.base_url = base_url.rstrip('/')
        self.files = {}
        self.albums = {'_new_': {'assets': None}}
        self.headers = {'x-api-key': self.api_key}

        self.load_albums()

    def load_albums(self):
        """Fetch the list of files and albums from the Immich API."""

        # Fetch albums
        response = requests.get(f'{self.base_url}/albums', headers=self.headers)
        response.raise_for_status()
        albums = response.json()
        for album in albums:
            self.albums[album['albumName']] = {
                'id': album['id'],
                'assets': None
            }

    def load_files(self, album):
        if self.albums[album]['assets'] is None:
            if album == '_new_':
                self.albums[album]['assets'] = []
                # Fetch photos not in any album via POST
                post_data = {"isNotInAlbum": True, "page": 1}
                while True:
                    response = requests.post(f'{self.base_url}/search/metadata', headers=self.headers, json=post_data)
                    response.raise_for_status()
                    resp = response.json()
                    self.albums[album]['assets'].extend(resp['assets']['items'])
                    if resp['assets']['nextPage'] is not None: 
                        post_data['page'] += 1
                    else:
                        break
            else:
                response = requests.get(f"{self.base_url}/albums/{self.albums[album]['id']}", headers=self.headers)
                response.raise_for_status()
                assets = response.json()['assets']
                self.albums[album]['assets'] = assets

    def getattr(self, path, fh=None):
        if path == '/':
            return dict(st_mode=(0o755 | 0o040000), st_nlink=2)
        elif path == '/_new_':
            return dict(st_mode=(0o755 | 0o040000), st_nlink=2)
        elif path[1:] in self.albums:
            return dict(st_mode=(0o755 | 0o040000), st_nlink=2)
        else:
            inalbum = os.path.dirname(path[1:])
            filename = os.path.basename(path)
            if inalbum in self.albums:
                self.load_files(inalbum)
                for asset in self.albums[inalbum]['assets']:
                    if filename == asset['originalFileName']:
                        return dict(
                            st_mode=(0o644 | 0o100000),
                            st_size=asset['exifInfo']['fileSizeInByte'],
                            st_nlink=1,
                            st_atime=datetime.fromisoformat(asset['localDateTime']).timestamp(),
                            st_mtime=datetime.fromisoformat(asset['fileModifiedAt']).timestamp(),
                            st_ctime=datetime.fromisoformat(asset['fileCreatedAt']).timestamp(),
                            st_uid=os.getuid(),
                            st_gid=os.getgid()
                        )
        raise OSError(errno.ENOENT, "No such file or directory")

    def readdir(self, path, fh):
        if path == '/':
            return ['.', '..', '_new_'] + [album for album in self.albums]
        else:
            album_name = os.path.basename(path)
            if album_name in self.albums:
                self.load_files(album_name)
                return ['.', '..'] + [asset['originalFileName'] for asset in self.albums[album_name]['assets']]
        raise OSError(errno.ENOENT, "No such file or directory")

    def open(self, path, flags):
        return 0

    @lru_cache(maxsize=128)
    def fetch_file_contents(self, path):
        inalbum = os.path.dirname(path[1:])
        filename = os.path.basename(path)
        if inalbum in self.albums:
            self.load_files(inalbum)
            for asset in self.albums[inalbum]['assets']:
                if filename == asset['originalFileName']:
                    response = requests.get(
                        f'{self.base_url}/assets/{asset["id"]}/original',
                        headers=self.headers,
                        stream=True
                    )
                    response.raise_for_status()
                    return response.content
        raise FileNotFoundError

    def read(self, path, size, offset, fh):
        return self.fetch_file_contents(path)[offset:offset + size]


def main():
    parser = argparse.ArgumentParser(description='Mount Immich photo server as a FUSE filesystem.')
    parser.add_argument('--api-key', required=True, help='Immich API key')
    parser.add_argument('--server', required=True, help='Immich server base URL (e.g., http://127.0.0.1:2283/api)')
    parser.add_argument('--mount-point', required=True, help='Mount point for the FUSE filesystem')

    args = parser.parse_args()

    mountpoint = args.mount_point
    os.makedirs(mountpoint, exist_ok=True)

    fuse = ImmichFuse(api_key=args.api_key, base_url=args.server)
    FUSE(fuse, mountpoint, nothreads=True, foreground=True)

if __name__ == '__main__':
    main()

