import fetch from 'node-fetch'
require('dotenv').config();
const {VUE_APP_GITHUB_ID, GH_APP_SECRET, GH_TOKEN_ENCRYPTION_KEY} = process.env;
const Cryptr = require('cryptr');
const cryptr = new Cryptr(GH_TOKEN_ENCRYPTION_KEY);

/**
 * @class GitHubAPI
 * @description The GitHubAPI backend is a lambda function invoked via Netlify's functions system. It receives HTTP requests and conducts GitHub API calls based on the instructions in the request header and data provided in the body.
 */

/**
 * Callback to return the results of the API interaction to the client. Returns a status of 500 if error is set, otherwise returns the status given in response.
 * @callback returnResponse
 * @param error {Error} An error encountered while using the API.
 * @param response {{statusText: string, body: string, statusCode: number}} HTTPResponse-like object returned to the client.
 * @return {void}
 */

/**
 * Process requests from a client.
 * @memberOf GitHubAPI
 * @param event {object} request details.
 * @param context {object} environment details.
 * @param callback {returnResponse} function to send the response to the client.
 */
async function main(event, context, callback) {
    try {
        console.log(`New request: task=${event.headers.task}`)
        switch (event.headers.task) {
            case "redeemCode":
                return callback(null, await redeemCode(event));
            case "getUserDetails":
                return callback(null, await getUserDetails(event));
            case "findRepositories":
                return callback(null, await findRepositories(event));
            case "findRepositoryFiles":
                return callback(null, await findRepositoryFiles(event));
            case "createRepository":
                return callback(null, await createRepository(event));
            case "pushFile":
                return callback(null, await pushFile(event));
            case "pullItem":
                return callback(null, await pullItem(event));
            case "setTopics":
                return callback(null, await setTopics(event));
            case "copyFile":
                return callback(null, await copyFile(event));
            case "deleteFile":
                return callback(null, await deleteFile(event));
            case "getLastBuild":
                return callback(null, await getLastBuild(event));
            default:
                if(event.headers.task)
                    throw new Error(`Unrecognised githubAPI task requested: ${event.headers.task}`);
                else
                    throw new Error("No githubAPI task specified");
        }
    } catch(e) {
        console.error(e);
        callback(e);
    }
}

/**
 * Send an OK response.
 * @memberOf GitHubAPI
 * @param obj {object} Body content to be JSON.stringified().
 * @return {{statusText: string, body: string, statusCode: number}}
 */
function OK(obj) {
    return {
        statusCode: 200, statusText: "OK",
        body: JSON.stringify(obj)
    };
}


/**
 * Check a response code matches one of the expected codes.
 * @memberOf GitHubAPI
 * @param response {object} GitHub API response.
 * @param code {number|number[]} status code to check for.
 * @param method {string} request method.
 * @return {Promise<object>}
 */
async function checkResponseCode(response, code, method = 'GET') {
    console.log(`${response.status} - ${response.statusText}: ${method} ${response.url.replace("https://api.github.com", "")}`);

    if(typeof code === 'number')
        code = [code];

    const json = await response.json();
    if(!code.includes(response.status))
        throw new Error(`${response.statusText} (${response.status}): ${json.message}`);
    return json;
}

/**
 * Send a code to GitHub and request a token in exchange.
 * @memberOf GitHubAPI
 * @param event {object} request details.
 * @return {{statusText: string, body: string, statusCode: number}}
 */
async function redeemCode(event) {
    console.log(`Exchanging code ${event.headers['github-code']} for token`)
    const url = `https://github.com/login/oauth/access_token?client_id=${VUE_APP_GITHUB_ID}&client_secret=${GH_APP_SECRET}&code=${event.headers["github-code"]}`;
    const access_token = await fetch(
        url,
        {headers: {"accept": "application/vnd.github.v3+json"}}
    )
        .then(r => checkResponseCode(r, 200, 'GET'))
        .then(json => json.access_token)
        .catch(e => {
            console.error(e)
            throw new Error(e)
        });

    return OK({access_token: cryptr.encrypt(access_token)});
}

/**
 * Get the GitHub user details.
 * @memberOf GitHubAPI
 * @param event {object} request details.
 * @return {{statusText: string, body: string, statusCode: number}}
 */
async function getUserDetails(event) {
    const d = JSON.parse(event.body);

    const details = await fetch('https://api.github.com/user', {headers: {
            "accept": "application/vnd.github.v3+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        }})
        .then(r => checkResponseCode(r, 200, 'GET'));
    return OK(details);
}

/**
 * Look up the repositories.
 * @memberOf GitHubAPI
 * @param event
 * @return {{statusText: string, body: string, statusCode: number}}
 */
async function findRepositories(event) {
    const d = JSON.parse(event.body);
    let topics = "";
    let user = "";
    if(d.topics)
        topics = '+' + d.topics.map(t => `topic:${t}`).join('+');
    if(d.owner)
        user = `+user:${d.owner}`;
    const url = `https://api.github.com/search/repositories?q=fork:true+topic:ukrn-open-research${user}${topics}`;
    console.log(`findRepositories(${url})`)
    const items = await fetch(url, {
        method: "GET", headers: {
            "accept": "application/vnd.github.mercy-preview+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        }
    })
        .then(r => checkResponseCode(r, 200, 'GET'))
        .then(json => json.items);
    return OK(items);
}

/**
 * Find all the files in a given repository.
 * @memberOf GitHubAPI
 * @param event
 * @return {{statusText: string, body: string, statusCode: number}}
 */
async function findRepositoryFiles(event) {
    const d = JSON.parse(event.body);
    const files = d.extraFiles.map(f => `${d.url}/contents/${f}`);
    const dirs = [
        d.includeEpisodes? "_episodes" : null,
        d.includeEpisodes? "_episodes_rmd" : null,
        d.extraFiles.length? "_includes/install_instructions" : null,
        d.extraFiles.length? "_includes/intro/optional" : null
    ]
        .filter(p => p !== null);

    // Fill in the file paths from the directory crawl
    const fileList = await Promise.all(dirs.map(dir => fetch(`${d.url}/contents/${dir}`, {
            method: "GET", headers: {
                "accept": "application/vnd.github.v3+json",
                "authorization": `token ${cryptr.decrypt(d.token)}`
            }
        })
            // Protect against 404 errors because some repos don't have some directories
            .then(r => r.status === 404? [{}] : checkResponseCode(r, 200, 'GET'))
            .then(json => json.map(i => {
                if(i.type === "file" && !/^[._]/.test(i.name))
                    return i.url;
                return null;
            }))
    ))
        .then(r => {
            const fList = files;
            r.forEach(L => fList.push(...L));
            return fList.filter(f => f !== null);
        });
    const response = await Promise.all(fileList.map(f => fetch(f, {
            method: "GET",
            headers: {
                "accept": "application/vnd.github.v3+json",
                "authorization": `token ${cryptr.decrypt(d.token)}`
            }
        })
            .then(r => r.status === 404? null : checkResponseCode(r, 200, 'GET'))
    ));
    return OK(response.filter(r => r !== null));
}

/**
 * Create a repository on GitHub for the currently authorised user.
 * @memberOf GitHubAPI
 * @param event {object} request details.
 * @return {{statusText: string, body: string, statusCode: number}}
 */
async function createRepository(event) {
    const d = JSON.parse(event.body);
    const newRepo = await fetch(`https://api.github.com/repos/${d.template}/generate`, {
        method: "POST",
        headers: {
            "accept": "application/vnd.github.baptiste-preview+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        },
        body: JSON.stringify({
            name: d.name,
            private: false
        })
    })
        .then(r => checkResponseCode(r, 201, 'POST'));
    await fetch(`${newRepo.url}/topics`, {
        method: "PUT",
        headers: {
            "accept": "application/vnd.github.mercy-preview+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        },
        body: JSON.stringify({
            names: ['ukrn-open-research', 'ukrn-workshop']
        })
    })
        .then(r => checkResponseCode(r, 200, 'PUT'));
    // Fetch a fresh copy with the updated topics
    return pullItem({body: JSON.stringify({
            url: newRepo.url, token: d.token
        })});
}

/**
 * Replace a file with a new version via github commit.
 * @memberOf GitHubAPI
 * @param event {object} request details.
 * @return {{statusText: string, body: string, statusCode: number}}
 */
async function pushFile(event) {
    const d = JSON.parse(event.body);
    const upload = await fetch(d.url, {
        method: "PUT",
        headers: {
            "accept": "application/vnd.github.mercy-preview+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        },
        body: JSON.stringify({
            content: d.content,
            message: `${d.path} update by UKRN Workshop Builder`,
            sha: d.sha
        })
    })
        .then(r => checkResponseCode(r, [200, 201], 'PUT'));
    // retrieve updated version
    const file = await fetch(upload.content.url, {
        method: "GET", headers: {
            "accept": "application/vnd.github.mercy-preview+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        }
    })
        .then(r => checkResponseCode(r, 200, 'GET'));
    return OK(file);
}

/**
 * Pull an item from GitHub by its URL.
 * @memberOf GitHubAPI
 * @param event
 * @return {{statusText: string, body: string, statusCode: number}}
 */
async function pullItem(event) {
    const d = JSON.parse(event.body);
    const item = await fetch(d.url, {
        method: "GET",
        headers: {
            "accept": "application/vnd.github.mercy-preview+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        }
    })
        .then(r => checkResponseCode(r, 200, 'GET'));
    return OK(item);
}

/**
 * Set the topics on a newly created workshop so we can check custom repository submissions' eligibility easily.
 * @memberOf GitHubAPI
 * @param event {object} request details.
 * @return {{statusText: string, body: string, statusCode: number}}
 */
async function setTopics(event) {
    const d = JSON.parse(event.body);
    // Find current topics
    const topics = await getTopics(event);
    const setTopics = d.topics;
    // To protect against removal of customised topics we use this approach
    topics.forEach(t => {
        if(!Object.keys(setTopics).includes(t))
            setTopics[t] = true;
    });
    const newTopicList = Object.keys(setTopics).filter(k => setTopics[k]);

    // Send the topic list
    await fetch(`${d.url}/topics`, {
        method: "PUT",
        headers: {
            "accept": "application/vnd.github.mercy-preview+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        },
        body: JSON.stringify({names: newTopicList})
    })
        .then(r => checkResponseCode(r, 200, 'PUT'));
    // Fetch final topic list for sanity
    return OK(await getTopics(event));
}

/**
 * Get the topics associated with a repository.
 * @memberOf GitHubAPI
 * @param event {{body: string, ...*:any}}
 * @return {Promise<Object>}
 */
async function getTopics(event) {
    const d = JSON.parse(event.body);
    const topics = await fetch(`${d.url}/topics`, {
        method: "GET",
        headers: {
            "accept": "application/vnd.github.mercy-preview+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        }
    })
        .then(r => checkResponseCode(r, 200, 'GET'));
    return topics.names;
}

/**
 * Copy a file from one repository to another, and return the copy.
 * @memberOf GitHubAPI
 * @param event {{body: string, ...*:any}}
 * @return {Promise<Object>}
 */
async function copyFile(event) {
    const d = JSON.parse(event.body);
    let file = null;
    // Check if the target url already exists
    if(d.returnExisting) {
        const existing = await fetch(d.newURL, {
            method: "GET",
            headers: {
                "accept": "application/vnd.github.mercy-preview+json",
                "authorization": `token ${cryptr.decrypt(d.token)}`
            }
        })
            .then(r => checkResponseCode(r, 200, 'GET'))
            .then(json => json.content)
            .catch(() => null);
        if(existing)
            return OK(existing);
    }

    return await fetch(d.url, {
        method: "GET",
        headers: {
            "accept": "application/vnd.github.mercy-preview+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        }
    })
        .then(r => checkResponseCode(r, 200, 'GET'))
        .then(json => file = json)
        .then(() => pushFile({body: JSON.stringify({
                ...d, content: file.content, path: file.path, url: d.newURL
            })}));
}

/**
 * Delete a file via github commit.
 * @memberOf GitHubAPI
 * @param event {object} request details.
 * @return {{statusText: string, body: string, statusCode: number}}
 */
async function deleteFile(event) {
    const d = JSON.parse(event.body);
    const file = await fetch(d.url, {
        method: "GET",
        headers: {
            "accept": "application/vnd.github.v3+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        }
    })
        .then(r => checkResponseCode(r, 200, 'GET'));

    return OK(
        await fetch(file.url, {
            method: "DELETE",
            headers: {
                "accept": "application/vnd.github.v3+json",
                "authorization": `token ${cryptr.decrypt(d.token)}`
            },
            body: JSON.stringify({
                message: `${file.path} deleted by UKRN Workshop Builder`,
                sha: file.sha
            })
        })
            .then(r => checkResponseCode(r, 200, 'DELETE'))
    );
}

/**
 * Retrieve the build status report for the last GitHub Pages build attempt.
 * @memberOf GitHubAPI
 * @param event {object} Should have body JSON string with repository URL and GitHub access token.
 * @return {Promise<{statusText: string, body: string, statusCode: number}>}
 */
async function getLastBuild(event) {
    const d = JSON.parse(event.body);
    const status = await fetch(`${d.url}/pages/builds/latest`, {
        headers: {
            "accept": "application/vnd.github.v3+json",
            "authorization": `token ${cryptr.decrypt(d.token)}`
        }})
        .then(r => checkResponseCode(r, 200, 'GET'))
    return OK(status);
}

if(process.env.NODE_ENV === 'test') {
    module.exports = {
        main,
        OK,
        checkResponseCode,
        redeemCode,
        getUserDetails,
        findRepositories,
        findRepositoryFiles,
        createRepository,
        pushFile,
        pullItem,
        setTopics,
        getTopics,
        copyFile,
        deleteFile,
        getLastBuild
    }
}
exports.handler = main;