import fs from 'fs';
import path from 'path';
import { z } from 'zod';
import { globalOptionsZod } from '../../../Command.js';
import request from '../../../request.js';
import { validation } from '../../../utils/validation.js';
import GraphCommand from '../../base/GraphCommand.js';
import commands from '../commands.js';
export const options = z.strictObject({
    ...globalOptionsZod.shape,
    folderUrl: z.string()
        .refine(url => validation.isValidSharePointUrl(url) === true, {
        error: e => `'${e.input}' is not a valid SharePoint Online folder URL.`
    })
        .alias('u'),
    filePath: z.string().alias('p'),
    siteUrl: z.string().optional()
});
class FileAddCommand extends GraphCommand {
    get name() {
        return commands.ADD;
    }
    get description() {
        return 'Uploads file to the specified site';
    }
    get schema() {
        return options;
    }
    getRefinedSchema(schema) {
        return schema
            .refine(options => fs.existsSync(options.filePath), {
            error: e => `Specified source file ${e.input.filePath} doesn't exist`
        })
            .refine(options => {
            if (options.siteUrl) {
                return validation.isValidSharePointUrl(options.siteUrl) === true;
            }
            return true;
        }, {
            error: e => `'${e.input.siteUrl}' is not a valid SharePoint Online site URL.`
        });
    }
    async commandAction(logger, args) {
        let folderUrlWithoutTrailingSlash = args.options.folderUrl;
        if (folderUrlWithoutTrailingSlash.endsWith('/')) {
            folderUrlWithoutTrailingSlash = folderUrlWithoutTrailingSlash.substring(0, folderUrlWithoutTrailingSlash.length - 1);
        }
        try {
            const graphFileUrl = await this.getGraphFileUrl(logger, `${folderUrlWithoutTrailingSlash}/${path.basename(args.options.filePath)}`, args.options.siteUrl);
            await this.uploadFile(args.options.filePath, graphFileUrl);
        }
        catch (err) {
            this.handleRejectedODataJsonPromise(err);
        }
    }
    /**
     * Uploads the specified local file to a document library using Microsoft Graph
     * @param localFilePath Path to the local file to upload
     * @param targetGraphFileUrl Graph drive item URL of the file to upload
     * @returns Absolute URL of the uploaded file
     */
    async uploadFile(localFilePath, targetGraphFileUrl) {
        const fileContents = fs.readFileSync(localFilePath);
        const isEmptyFile = fileContents.length === 0;
        const requestOptions = {
            url: isEmptyFile ? `${targetGraphFileUrl}:/content` : `${targetGraphFileUrl}:/createUploadSession`,
            headers: {
                accept: 'application/json;odata.metadata=none'
            },
            responseType: 'json'
        };
        if (isEmptyFile) {
            const res = await request.put(requestOptions);
            return res.webUrl;
        }
        else {
            const res = await request.post(requestOptions);
            const requestOptionsPut = {
                url: res.uploadUrl,
                headers: {
                    'x-anonymous': true,
                    'accept': 'application/json;odata.metadata=none',
                    'Content-Length': fileContents.length,
                    'Content-Range': `bytes 0-${fileContents.length - 1}/${fileContents.length}`
                },
                data: fileContents,
                responseType: 'json'
            };
            const resPut = await request.put(requestOptionsPut);
            return resPut.webUrl;
        }
    }
    /**
     * Gets Graph's drive item URL for the specified file. If the user specified
     * a local file to convert to PDF, returns the URL resolved while uploading
     * the file
     *
     * Example:
     *
     * fileWebUrl:
     * https://contoso.sharepoint.com/sites/Contoso/site/Shared%20Documents/file.docx
     *
     * returns:
     * https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,9d1b2174-9906-43ec-8c9e-f8589de047af,f60c833e-71ce-4a5a-b90e-2a7fdb718397/drives/b!k6NJ6ubjYEehsullOeFTcuYME3w1S8xHoHziURdWlu-DWrqz1yBLQI7E7_4TN6fL/root:/file.docx
     *
     * @param await logger.logger instance
     * @param fileWebUrl Web URL of the file for which to get drive item URL
     * @param siteUrl URL of the site to which upload the file. Optional. Specify to suppress lookup.
     * @returns Graph's drive item URL for the specified file
     */
    async getGraphFileUrl(logger, fileWebUrl, siteUrl) {
        if (this.debug) {
            await logger.logToStderr(`Resolving Graph drive item URL for ${fileWebUrl}`);
        }
        const _fileWebUrl = new URL(fileWebUrl);
        const _siteUrl = new URL(siteUrl || fileWebUrl);
        const isSiteUrl = typeof siteUrl !== 'undefined';
        const siteInfo = await this.getGraphSiteInfoFromFullUrl(_siteUrl.host, _siteUrl.pathname, isSiteUrl);
        let siteId = siteInfo.id;
        let siteRelativeFileUrl = _fileWebUrl.pathname.replace(siteInfo.serverRelativeUrl, '');
        // normalize site-relative URLs for root site collections and root sites
        if (!siteRelativeFileUrl.startsWith('/')) {
            siteRelativeFileUrl = '/' + siteRelativeFileUrl;
        }
        const siteRelativeFileUrlChunks = siteRelativeFileUrl.split('/');
        let driveRelativeFileUrl = `/${siteRelativeFileUrlChunks.slice(2).join('/')}`;
        // chunk 0 is empty because the URL starts with /
        const driveId = await this.getDriveId(logger, siteId, siteRelativeFileUrlChunks[1]);
        const graphUrl = `${this.resource}/v1.0/sites/${siteId}/drives/${driveId}/root:${driveRelativeFileUrl}`;
        if (this.debug) {
            await logger.logToStderr(`Resolved URL ${graphUrl}`);
        }
        return graphUrl;
    }
    /**
     * Retrieves the Graph ID and server-relative URL of the specified (sub)site.
     * Automatically detects which path chunks correspond to (sub)site.
     * @param hostName SharePoint host name, eg. contoso.sharepoint.com
     * @param urlPath Server-relative file URL, eg. /sites/site/docs/file1.aspx
     * @param isSiteUrl Set to true to indicate that the specified URL is a site URL
     * @returns ID and server-relative URL of the site denoted by urlPath
     */
    async getGraphSiteInfoFromFullUrl(hostName, urlPath, isSiteUrl) {
        const siteId = '';
        const urlChunks = urlPath.split('/');
        return await this.getGraphSiteInfo(hostName, urlChunks, isSiteUrl ? urlChunks.length - 1 : 0, siteId);
    }
    /**
     * Retrieves Graph site ID and server-relative URL of the site specified
     * using chunks from the URL path. Method is being called recursively as long
     * as it can successfully retrieve the site. When retrieving site fails, method
     * will return the last resolved site ID. If no site ID has been retrieved
     * (method fails on the first execution), it will call the reject callback.
     * @param hostName SharePoint host name, eg. contoso.sharepoint.com
     * @param urlChunks Array of chunks from server-relative URL, eg. ['sites', 'site', 'subsite', 'docs', 'file1.aspx']
     * @param currentChunk Current chunk that's being tested, eg. sites
     * @param lastSiteId Last correctly resolved Graph site ID
     * @returns Graph site ID and server-relative URL of the site specified through chunks
     */
    async getGraphSiteInfo(hostName, urlChunks, currentChunk, lastSiteId) {
        let currentPath = urlChunks.slice(0, currentChunk + 1).join('/');
        if (currentPath.endsWith('/sites') ||
            currentPath.endsWith('/teams') ||
            currentPath.endsWith('/personal')) {
            return await this.getGraphSiteInfo(hostName, urlChunks, currentChunk + 1, '');
        }
        if (!currentPath.startsWith('/')) {
            currentPath = '/' + currentPath;
        }
        const requestOptions = {
            url: `${this.resource}/v1.0/sites/${hostName}:${currentPath}?$select=id`,
            headers: {
                accept: 'application/json;odata.metadata=none'
            },
            responseType: 'json'
        };
        const getResult = (id, serverRelativeUrl) => {
            return {
                id,
                serverRelativeUrl
            };
        };
        try {
            const res = await request.get(requestOptions);
            if (currentChunk === urlChunks.length - 1) {
                return getResult(res.id, currentPath);
            }
            else {
                return await this.getGraphSiteInfo(hostName, urlChunks, ++currentChunk, res.id);
            }
        }
        catch (err) {
            if (lastSiteId) {
                let serverRelativeUrl = `${urlChunks.slice(0, currentChunk).join('/')}`;
                if (!serverRelativeUrl.startsWith('/')) {
                    serverRelativeUrl = '/' + serverRelativeUrl;
                }
                return getResult(lastSiteId, serverRelativeUrl);
            }
            else {
                throw err;
            }
        }
    }
    /**
     * Returns the Graph drive ID of the specified document library
     * @param graphSiteId Graph ID of the site where the document library is located
     * @param siteRelativeListUrl Server-relative URL of the document library, eg. /sites/site/Documents
     * @returns Graph drive ID of the specified document library
     */
    async getDriveId(logger, graphSiteId, siteRelativeListUrl) {
        const requestOptions = {
            url: `${this.resource}/v1.0/sites/${graphSiteId}/drives?$select=webUrl,id`,
            headers: {
                accept: 'application/json;odata.metadata=none'
            },
            responseType: 'json'
        };
        const res = await request.get(requestOptions);
        if (this.debug) {
            await logger.logToStderr(`Searching for drive with a URL ending with /${siteRelativeListUrl}...`);
        }
        const drive = res.value.find(d => d.webUrl.endsWith(`/${siteRelativeListUrl}`));
        if (!drive) {
            throw 'Drive not found';
        }
        return drive.id;
    }
}
export default new FileAddCommand();
//# sourceMappingURL=file-add.js.map