"use strict";
// backend/src/backup/providers/dropbox.ts
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.DropboxProvider = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const dropbox_1 = require("dropbox");
const TOKEN_ENDPOINT = 'https://api.dropboxapi.com/oauth2/token';
// 150 MB threshold: above this use chunked upload session
const SIMPLE_UPLOAD_MAX = 150 * 1024 * 1024;
// 8 MB chunk size for upload sessions
const CHUNK_SIZE = 8 * 1024 * 1024;
class DropboxProvider {
    constructor(config) {
        this.dbx = null;
        this.config = config;
        if (config.tokens) {
            this.initClient(config.tokens.access_token);
        }
    }
    initClient(accessToken) {
        this.dbx = new dropbox_1.Dropbox({ accessToken });
    }
    client() {
        if (!this.dbx) {
            throw new Error('Dropbox client is not initialized');
        }
        return this.dbx;
    }
    /**
     * Normalize a path to Dropbox format:
     * - Root is empty string ''
     * - All other paths start with '/' and have no trailing slash
     */
    normPath(remotePath) {
        // Strip leading/trailing slashes then re-add leading slash if non-empty
        const stripped = remotePath.replace(/^\/+|\/+$/g, '');
        if (!stripped)
            return '';
        return '/' + stripped;
    }
    getAuthUrl(redirectUri, state) {
        const params = new URLSearchParams({
            client_id: this.config.clientId,
            response_type: 'code',
            redirect_uri: redirectUri,
            token_access_type: 'offline',
            ...(state ? { state } : {}),
        });
        return `https://www.dropbox.com/oauth2/authorize?${params.toString()}`;
    }
    async handleCallback(code, redirectUri) {
        const body = new URLSearchParams({
            code,
            grant_type: 'authorization_code',
            redirect_uri: redirectUri,
        });
        const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
        const res = await fetch(TOKEN_ENDPOINT, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                Authorization: `Basic ${credentials}`,
            },
            body: body.toString(),
        });
        if (!res.ok) {
            const text = await res.text();
            throw new Error(`Dropbox token exchange failed: ${res.status} ${text}`);
        }
        const data = (await res.json());
        if (!data.access_token || !data.refresh_token) {
            throw new Error('Missing tokens in Dropbox OAuth2 callback response');
        }
        const providerTokens = {
            access_token: data.access_token,
            refresh_token: data.refresh_token,
            expiry: new Date(Date.now() + data.expires_in * 1000).toISOString(),
        };
        this.config.tokens = providerTokens;
        this.initClient(providerTokens.access_token);
        return providerTokens;
    }
    async refreshToken() {
        if (!this.config.tokens?.refresh_token) {
            throw new Error('No refresh token available for Dropbox provider');
        }
        const body = new URLSearchParams({
            grant_type: 'refresh_token',
            refresh_token: this.config.tokens.refresh_token,
        });
        const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
        const res = await fetch(TOKEN_ENDPOINT, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                Authorization: `Basic ${credentials}`,
            },
            body: body.toString(),
        });
        if (!res.ok) {
            const text = await res.text();
            throw new Error(`Dropbox token refresh failed: ${res.status} ${text}`);
        }
        const data = (await res.json());
        if (!data.access_token) {
            throw new Error('Failed to refresh Dropbox access token');
        }
        const providerTokens = {
            access_token: data.access_token,
            refresh_token: data.refresh_token ?? this.config.tokens.refresh_token,
            expiry: new Date(Date.now() + data.expires_in * 1000).toISOString(),
        };
        this.config.tokens = providerTokens;
        this.initClient(providerTokens.access_token);
        return providerTokens;
    }
    isAuthorized() {
        return !!(this.config.tokens?.access_token && this.config.tokens?.refresh_token);
    }
    async ensureAuth() {
        if (!this.isAuthorized()) {
            throw new Error('Dropbox provider is not authorized');
        }
        const tokens = this.config.tokens;
        const expiryMs = new Date(tokens.expiry).getTime();
        const nowMs = Date.now();
        // Refresh if within 60 seconds of expiry
        if (expiryMs - nowMs < 60 * 1000) {
            await this.refreshToken();
        }
    }
    async listFiles(remotePath) {
        await this.ensureAuth();
        const dbxPath = this.normPath(remotePath);
        const results = [];
        let response = await this.client().filesListFolder({ path: dbxPath });
        while (true) {
            for (const entry of response.result.entries) {
                const isDirectory = entry['.tag'] === 'folder';
                const entryPath = entry.path_display ?? entry.path_lower ?? entry.name;
                // Return path relative to remotePath base (strip leading slash)
                const normalizedBase = remotePath.replace(/^\/+|\/+$/g, '');
                const relPath = normalizedBase
                    ? `${normalizedBase}/${entry.name}`
                    : entry.name;
                results.push({
                    name: entry.name,
                    path: relPath,
                    isDirectory,
                    size: isDirectory ? 0 : (entry.size ?? 0),
                    modifiedTime: isDirectory
                        ? new Date().toISOString()
                        : (entry.server_modified ?? new Date().toISOString()),
                });
            }
            if (!response.result.has_more)
                break;
            response = await this.client().filesListFolderContinue({
                cursor: response.result.cursor,
            });
        }
        return results;
    }
    async uploadFile(localPath, remotePath) {
        await this.ensureAuth();
        const dbxPath = this.normPath(remotePath);
        const stat = fs.statSync(localPath);
        const fileSize = stat.size;
        if (fileSize < SIMPLE_UPLOAD_MAX) {
            // Simple upload
            const fileContent = fs.readFileSync(localPath);
            await this.client().filesUpload({
                path: dbxPath,
                mode: { '.tag': 'overwrite' },
                contents: fileContent,
            });
        }
        else {
            // Chunked upload session
            const fd = fs.openSync(localPath, 'r');
            try {
                // Start upload session with first chunk
                let offset = 0;
                const firstChunkSize = Math.min(CHUNK_SIZE, fileSize);
                const firstChunk = Buffer.alloc(firstChunkSize);
                fs.readSync(fd, firstChunk, 0, firstChunkSize, 0);
                offset += firstChunkSize;
                const startRes = await this.client().filesUploadSessionStart({
                    close: false,
                    contents: firstChunk,
                });
                const sessionId = startRes.result.session_id;
                // Append remaining chunks
                while (offset < fileSize) {
                    const chunkSize = Math.min(CHUNK_SIZE, fileSize - offset);
                    const chunk = Buffer.alloc(chunkSize);
                    fs.readSync(fd, chunk, 0, chunkSize, offset);
                    const isLast = offset + chunkSize >= fileSize;
                    if (isLast) {
                        // Finish the session
                        await this.client().filesUploadSessionFinish({
                            cursor: { session_id: sessionId, offset },
                            commit: {
                                path: dbxPath,
                                mode: { '.tag': 'overwrite' },
                            },
                            contents: chunk,
                        });
                    }
                    else {
                        await this.client().filesUploadSessionAppendV2({
                            cursor: { session_id: sessionId, offset },
                            close: false,
                            contents: chunk,
                        });
                    }
                    offset += chunkSize;
                }
                // Edge case: file exactly one chunk — finish with empty content
                if (fileSize === firstChunkSize) {
                    await this.client().filesUploadSessionFinish({
                        cursor: { session_id: sessionId, offset },
                        commit: {
                            path: dbxPath,
                            mode: { '.tag': 'overwrite' },
                        },
                        contents: Buffer.alloc(0),
                    });
                }
            }
            finally {
                fs.closeSync(fd);
            }
        }
    }
    async deleteFile(remotePath) {
        await this.ensureAuth();
        const dbxPath = this.normPath(remotePath);
        try {
            await this.client().filesDeleteV2({ path: dbxPath });
        }
        catch (err) {
            // Ignore path_lookup errors (file not found) — treat as success
            const errObj = err;
            const summary = errObj?.error?.error_summary ?? '';
            if (!summary.startsWith('path_lookup/')) {
                throw err;
            }
        }
    }
    async mkdir(remotePath) {
        await this.ensureAuth();
        const dbxPath = this.normPath(remotePath);
        if (!dbxPath)
            return; // root always exists
        try {
            await this.client().filesCreateFolderV2({ path: dbxPath });
        }
        catch (err) {
            // Ignore path/conflict errors (folder already exists)
            const errObj = err;
            const summary = errObj?.error?.error_summary ?? '';
            if (!summary.startsWith('path/conflict')) {
                throw err;
            }
        }
    }
    async downloadFile(remotePath, localPath) {
        await this.ensureAuth();
        const dbxPath = this.normPath(remotePath);
        const res = await this.client().filesDownload({ path: dbxPath });
        // The SDK returns the binary content as `fileBinary` in the result
        const fileBinary = res.result.fileBinary;
        if (!fileBinary) {
            throw new Error(`Dropbox filesDownload returned no fileBinary for: ${remotePath}`);
        }
        // Ensure parent directory exists
        const parentDir = path.dirname(localPath);
        if (!fs.existsSync(parentDir)) {
            fs.mkdirSync(parentDir, { recursive: true });
        }
        fs.writeFileSync(localPath, fileBinary);
    }
}
exports.DropboxProvider = DropboxProvider;
//# sourceMappingURL=dropbox.js.map