'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var inquirer = require('inquirer');
var fetch = require('node-fetch');
var sdk = require('@linear/sdk');
var dateFns = require('date-fns');
var chalk = require('chalk');
var _ = require('lodash');
var cliProgress = require('cli-progress');
var ora = require('ora');

function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }

function _interopNamespace(e) {
    if (e && e.__esModule) return e;
    var n = Object.create(null);
    if (e) {
        Object.keys(e).forEach(function (k) {
            if (k !== 'default') {
                var d = Object.getOwnPropertyDescriptor(e, k);
                Object.defineProperty(n, k, d.get ? d : {
                    enumerable: true,
                    get: function () { return e[k]; }
                });
            }
        });
    }
    n["default"] = e;
    return Object.freeze(n);
}

var inquirer__namespace = /*#__PURE__*/_interopNamespace(inquirer);
var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
var chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk);
var ___default = /*#__PURE__*/_interopDefaultLegacy(_);
var ora__default = /*#__PURE__*/_interopDefaultLegacy(ora);

/******************************************************************************
Copyright (c) Microsoft Corporation.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */

function __rest(s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
}

function __awaiter(thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
}

typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
    var e = new Error(message);
    return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};

const GITHUB_API = "https://api.github.com/graphql";
const githubClient = (apiKey) => {
    return (query, variables) => __awaiter(void 0, void 0, void 0, function* () {
        const res = yield fetch__default["default"](GITHUB_API, {
            method: "POST",
            headers: {
                authorization: `token ${apiKey}`,
            },
            body: JSON.stringify({
                query,
                variables,
            }),
        });
        const data = yield res.json();
        return data.data;
    });
};

/**
 * Fetch and paginate through all Github issues.
 *
 * @param apiKey GitHub api key for authentication
 */
class GithubImporter {
    constructor(apiKey, owner, repo) {
        this.import = () => __awaiter(this, void 0, void 0, function* () {
            let issueData = [];
            let cursor;
            const github = githubClient(this.apiKey);
            while (true) {
                try {
                    const data = (yield github(`query lastIssues($owner: String!, $repo: String!, $num: Int, $cursor: String) {
            repository(owner:$owner, name:$repo) {
              issues(first:$num, after: $cursor, states:OPEN) {
                edges {
                  node {
                    id
                    title
                    body
                    url
                    createdAt
                    labels(first:100) {
                      nodes{
                        id
                        color
                        name
                        description
                      }
                    }
                    comments(first: 100) {
                      nodes {
                        id
                        body
                        createdAt
                        url
                        author {
                          login
                          avatarUrl(size: 255)
                          ... on User {
                            id
                            name
                            email
                          }                        
                        }
                      }
                    }
                  }
                }
                pageInfo {
                  hasNextPage
                  endCursor
                }
              }
            }
          }`, {
                        owner: this.owner,
                        repo: this.repo,
                        num: 25,
                        cursor,
                    }));
                    // User didn't select repo scope
                    if (!data || !data.repository) {
                        throw new Error(`Unable to find repo ${this.owner}/${this.repo}. Did you select \`repo\` scope for your GitHub token?`);
                    }
                    cursor = data.repository.issues.pageInfo.endCursor;
                    const fetchedIssues = data.repository.issues.edges.map((d) => d.node);
                    issueData = issueData.concat(fetchedIssues);
                    if (!data.repository.issues.pageInfo.hasNextPage) {
                        break;
                    }
                }
                catch (err) {
                    console.error(err);
                }
            }
            const importData = {
                issues: [],
                labels: {},
                users: {},
            };
            for (const issue of issueData) {
                importData.issues.push({
                    title: issue.title,
                    description: `${issue.body}\n\n[View original issue in GitHub](${issue.url})`,
                    url: issue.url,
                    comments: issue.comments.nodes
                        ? issue.comments.nodes
                            .filter(comment => comment.author.id)
                            .map(comment => ({
                            body: comment.body,
                            userId: comment.author.id,
                            createdAt: new Date(comment.createdAt),
                        }))
                        : [],
                    labels: issue.labels.nodes ? issue.labels.nodes.map(label => label.id) : [],
                    createdAt: new Date(issue.createdAt),
                });
                const users = issue.comments.nodes
                    ? issue.comments.nodes.map(comment => ({
                        id: comment.author.id,
                        name: comment.author.login,
                        avatarUrl: comment.author.avatarUrl,
                        email: comment.author.email,
                    }))
                    : [];
                for (const user of users) {
                    const { id, email } = user, userData = __rest(user, ["id", "email"]);
                    if (id) {
                        importData.users[id] = Object.assign(Object.assign({}, userData), { email: email && email.length > 0 ? email : undefined });
                    }
                }
                const labels = issue.labels.nodes
                    ? issue.labels.nodes.map(label => ({
                        id: label.id,
                        color: `#${label.color}`,
                        name: label.name,
                        description: label.description,
                    }))
                    : [];
                for (const label of labels) {
                    const { id } = label, labelData = __rest(label, ["id"]);
                    importData.labels[id] = labelData;
                }
            }
            return importData;
        });
        this.apiKey = apiKey;
        this.owner = owner;
        this.repo = repo;
    }
    get name() {
        return "GitHub";
    }
    get defaultTeamName() {
        return this.repo;
    }
}

const githubImport = () => __awaiter(void 0, void 0, void 0, function* () {
    const answers = yield inquirer__namespace.prompt(questions);
    const [owner, repo] = answers.repo.split("/");
    const githubImporter = new GithubImporter(answers.githubApiKey, owner, repo);
    return githubImporter;
});
const questions = [
    {
        type: "input",
        name: "githubApiKey",
        message: "Input your personal GitHub access token (https://github.com/settings/tokens, select `repo` scope)",
    },
    {
        type: "input",
        name: "repo",
        message: 'From which repo do you want to import issues from (e.g. "facebook/react")',
    },
];

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
 * String replace with async function.
 */
const replaceAsync = (str, regex, asyncFn) => __awaiter(void 0, void 0, void 0, function* () {
    const promises = [];
    // @ts-ignore
    str.replace(regex, (match, ...args) => {
        // @ts-ignore
        const promise = asyncFn(match, ...args);
        promises.push(promise);
    });
    const data = yield Promise.all(promises);
    return str.replace(regex, () => data.shift());
});

const IMAGE_MD_REGEX = /(?:!\[(.*?)\]\((https?:\/\/.*?)\))/;
const IMAGE_TAG_REGEX = /(?:<img.*?src=\"(.*?)\".*?>)/;
/**
 * Replace markdown image URLs with Linear uploaded ones.
 *
 * Also supports image tags used by GitHub.
 *
 * @param client Linear API client.
 * @param text Markdown content.
 * @param urlSuffix A suffix to append to each image URL, such as to perform authentication
 * @returns Markdown content with images using the new Linear URLs
 */
const replaceImagesInMarkdown = (client, text, urlSuffix) => __awaiter(void 0, void 0, void 0, function* () {
    let result = text;
    const effectiveURLSuffix = urlSuffix || "";
    // Markdown tags
    result = yield replaceAsync(result, IMAGE_MD_REGEX, (match, ...args) => __awaiter(void 0, void 0, void 0, function* () {
        const title = args[0];
        const url = args[1];
        const uploadedUrl = yield replaceImageUrl(client, url + effectiveURLSuffix);
        return `\n![${title}](${uploadedUrl})\n`;
    }));
    // HTML tags
    result = yield replaceAsync(result, IMAGE_TAG_REGEX, (match, ...args) => __awaiter(void 0, void 0, void 0, function* () {
        const url = args[0];
        const uploadedUrl = yield replaceImageUrl(client, url + effectiveURLSuffix);
        return `![](${uploadedUrl})`;
    }));
    return result;
});
/**
 * Downloads image and upload it to
 *
 * @param client Linear API client
 * @param url URL of the source image
 * @returns URL of the uploaded image
 */
const replaceImageUrl = (client, url) => __awaiter(void 0, void 0, void 0, function* () {
    try {
        const res = yield client.imageUploadFromUrl(url);
        if (res === null || res === void 0 ? void 0 : res.url) {
            return res === null || res === void 0 ? void 0 : res.url;
        }
    }
    catch (err) {
        console.error(`Failed to replace image`, err.message);
    }
    return url;
});

const defaultStateColors = {
    backlog: "#bec2c8",
    started: "#f2c94c",
    completed: "#5e6ad2",
};
/**
 * Import issues into Linear via the API.
 */
const importIssues = (apiKey, importer) => __awaiter(void 0, void 0, void 0, function* () {
    var _a, _b, _c, _d, _e, _f, _g;
    const client = new sdk.LinearClient({ apiKey });
    const importData = yield importer.import();
    const viewerQuery = yield client.viewer;
    let spinner = ora__default["default"]("Fetching teams and users").start();
    const allTeams = yield client.paginate(client.teams, {});
    const allUsers = yield client.paginate(client.users, { includeDisabled: false });
    spinner.stop();
    const viewer = viewerQuery === null || viewerQuery === void 0 ? void 0 : viewerQuery.id;
    // Prompt the user to either get or create a team
    const importAnswers = yield inquirer__namespace.prompt([
        {
            type: "confirm",
            name: "newTeam",
            message: "Do you want to create a new team for imported issues?",
            default: true,
        },
        {
            type: "input",
            name: "teamName",
            message: "Name of the team:",
            default: importer.defaultTeamName || importer.name,
            when: (answers) => {
                return answers.newTeam;
            },
        },
        {
            type: "list",
            name: "targetTeamId",
            message: "Import into team:",
            choices: () => __awaiter(void 0, void 0, void 0, function* () {
                return allTeams.map(team => ({
                    name: `[${team.key}] ${team.name}`,
                    value: team.id,
                }));
            }),
            when: (answers) => {
                return !answers.newTeam;
            },
        },
        {
            type: "confirm",
            name: "includeProject",
            message: "Do you want to import to a specific project?",
            when: (answers) => __awaiter(void 0, void 0, void 0, function* () {
                var _h;
                // if no team is selected then don't show projects screen
                if (!answers.targetTeamId) {
                    return false;
                }
                const team = yield client.team(answers.targetTeamId);
                const teamProjects = yield (team === null || team === void 0 ? void 0 : team.projects());
                const projects = (_h = teamProjects === null || teamProjects === void 0 ? void 0 : teamProjects.nodes) !== null && _h !== void 0 ? _h : [];
                return projects.length > 0;
            }),
        },
        {
            type: "list",
            name: "targetProjectId",
            message: "Import into project:",
            choices: (answers) => __awaiter(void 0, void 0, void 0, function* () {
                var _j;
                // if no team is selected then don't show projects screen
                if (!answers.targetTeamId) {
                    return false;
                }
                const team = yield client.team(answers.targetTeamId);
                const teamProjects = yield (team === null || team === void 0 ? void 0 : team.projects());
                const projects = (_j = teamProjects === null || teamProjects === void 0 ? void 0 : teamProjects.nodes) !== null && _j !== void 0 ? _j : [];
                return projects.map(project => ({
                    name: project.name,
                    value: project.id,
                }));
            }),
            when: (answers) => {
                return answers.includeProject;
            },
        },
        {
            type: "confirm",
            name: "includeComments",
            message: "Do you want to include comments in the issue description?",
            when: () => {
                return !!importData.issues.find(issue => issue.comments && issue.comments.length > 0);
            },
        },
        {
            type: "confirm",
            name: "selfAssign",
            message: "Do you want to assign these issues to yourself?",
            default: true,
        },
        {
            type: "list",
            name: "targetAssignee",
            message: "Assign to user:",
            choices: () => {
                const map = allUsers.map(user => ({
                    name: user.name,
                    value: user.id,
                }));
                map.unshift({ name: "[Unassigned]", value: "" });
                map.unshift({ name: "[Provided assignee]", value: "{{assignee}}" });
                return map;
            },
            when: (answers) => {
                return !answers.selfAssign;
            },
        },
    ]);
    let teamKey;
    let teamId;
    if (importAnswers.newTeam) {
        // Create a new team
        const teamResponse = yield client.createTeam({
            name: importAnswers.teamName,
        });
        const team = yield (teamResponse === null || teamResponse === void 0 ? void 0 : teamResponse.team);
        teamKey = team === null || team === void 0 ? void 0 : team.key;
        teamId = team === null || team === void 0 ? void 0 : team.id;
    }
    else {
        // Use existing team
        const existingTeam = allTeams === null || allTeams === void 0 ? void 0 : allTeams.find(team => team.id === importAnswers.targetTeamId);
        teamKey = existingTeam === null || existingTeam === void 0 ? void 0 : existingTeam.key;
        teamId = importAnswers.targetTeamId;
    }
    if (!teamId) {
        throw new Error("No team id found");
    }
    const teamInfo = yield client.team(teamId);
    const organization = yield client.organization;
    const existingLabels = [];
    spinner = ora__default["default"]("Fetching labels").start();
    const allTeamLabels = yield teamInfo.paginate(teamInfo.labels, {});
    const allWorkspaceLabels = yield client.paginate(organization.labels, {});
    existingLabels.push(...allTeamLabels, ...allWorkspaceLabels);
    spinner.stop();
    spinner = ora__default["default"]("Fetching workflow states").start();
    const workflowStates = yield (teamInfo === null || teamInfo === void 0 ? void 0 : teamInfo.states());
    const existingLabelMap = {};
    const existingLabelGroupsMap = {};
    for (const label of existingLabels) {
        const labelName = (_a = label.name) === null || _a === void 0 ? void 0 : _a.toLowerCase();
        if (label.isGroup) {
            if (labelName && label.id && !existingLabelGroupsMap[labelName]) {
                existingLabelGroupsMap[labelName] = label.id;
            }
        }
        else {
            if (labelName && label.id && !existingLabelMap[labelName]) {
                existingLabelMap[labelName] = label.id;
            }
        }
    }
    const projectId = importAnswers.targetProjectId;
    // Create labels and mapping to source data
    const labelMapping = {};
    for (const labelId of Object.keys(importData.labels)) {
        const label = importData.labels[labelId];
        let labelName = ___default["default"].truncate(label.name.trim(), { length: 20 });
        // Check if this label matches with an existing group label
        let actualLabelId = existingLabelGroupsMap[labelName.toLowerCase()];
        if (actualLabelId) {
            // This label has matched with an existing group label. We cannot re-use the label as-is, it will be renamed.
            actualLabelId = undefined;
            labelName = `${labelName} (imported)`;
        }
        // Check if this label matches with an existing label
        actualLabelId = existingLabelMap[labelName.toLowerCase()];
        if (!actualLabelId) {
            const labelResponse = yield client.createIssueLabel({
                name: labelName,
                description: label.description,
                color: label.color,
                teamId,
            });
            const issueLabel = yield (labelResponse === null || labelResponse === void 0 ? void 0 : labelResponse.issueLabel);
            if (issueLabel === null || issueLabel === void 0 ? void 0 : issueLabel.id) {
                actualLabelId = issueLabel === null || issueLabel === void 0 ? void 0 : issueLabel.id;
            }
            existingLabelMap[labelName.toLowerCase()] = actualLabelId;
        }
        labelMapping[labelId] = actualLabelId;
    }
    const existingStateMap = {};
    for (const state of (_b = workflowStates === null || workflowStates === void 0 ? void 0 : workflowStates.nodes) !== null && _b !== void 0 ? _b : []) {
        const stateName = (_c = state.name) === null || _c === void 0 ? void 0 : _c.toLowerCase();
        if (stateName && state.id && !existingStateMap[stateName]) {
            existingStateMap[stateName] = state.id;
        }
    }
    const existingUserMapByName = {};
    const existingUserMapByEmail = {};
    for (const user of allUsers) {
        const userName = (_d = user.name) === null || _d === void 0 ? void 0 : _d.toLowerCase();
        if (userName && !existingUserMapByName[userName]) {
            existingUserMapByName[userName] = user.id;
        }
        if (!existingUserMapByEmail[user.email]) {
            existingUserMapByEmail[user.email] = user.id;
        }
    }
    spinner.stop();
    const issuesProgressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
    issuesProgressBar.start(importData.issues.length, 0);
    let issueCursor = 0;
    // Create issues
    for (const issue of importData.issues) {
        const issueDescription = issue.description
            ? yield replaceImagesInMarkdown(client, issue.description, importData.resourceURLSuffix)
            : undefined;
        const description = importAnswers.includeComments && issue.comments
            ? yield buildComments(client, issueDescription || "", issue.comments, importData)
            : issueDescription;
        const labelIds = issue.labels ? _.uniq(issue.labels.map(labelId => labelMapping[labelId])) : undefined;
        let stateId = !!issue.status ? existingStateMap[issue.status.toLowerCase()] : undefined;
        // Create a new state since one doesn't already exist with this name
        if (!stateId && issue.status) {
            let stateType = "backlog";
            if (issue.completedAt) {
                stateType = "completed";
            }
            else if (issue.startedAt) {
                stateType = "started";
            }
            const newStateResult = yield client.createWorkflowState({
                name: issue.status,
                teamId,
                color: defaultStateColors[stateType],
                type: stateType,
            });
            if (newStateResult === null || newStateResult === void 0 ? void 0 : newStateResult.success) {
                const newState = yield newStateResult.workflowState;
                if (newState === null || newState === void 0 ? void 0 : newState.id) {
                    existingStateMap[issue.status.toLowerCase()] = newState.id;
                    stateId = newState.id;
                }
            }
        }
        const issueAssigneeId = (_e = issue.assigneeId) === null || _e === void 0 ? void 0 : _e.toLowerCase();
        const existingAssigneeId = !!issueAssigneeId
            ? (_f = existingUserMapByEmail[issueAssigneeId]) !== null && _f !== void 0 ? _f : existingUserMapByName[issueAssigneeId]
            : undefined;
        let assigneeId;
        if (importAnswers.selfAssign) {
            assigneeId = viewer;
        }
        else if (importAnswers.targetAssignee === "{{assignee}}") {
            assigneeId = existingAssigneeId;
        }
        else {
            assigneeId = importAnswers.targetAssignee || undefined;
        }
        const formattedDueDate = issue.dueDate ? dateFns.format(issue.dueDate, "yyyy-MM-dd") : undefined;
        const createdIssue = yield createIssueWithRetries(client, {
            teamId,
            projectId: projectId,
            title: issue.title,
            description,
            priority: issue.priority,
            labelIds,
            stateId,
            assigneeId,
            createdAt: issue.createdAt,
            dueDate: formattedDueDate,
        });
        if (issue.archived) {
            yield ((_g = (yield createdIssue.issue)) === null || _g === void 0 ? void 0 : _g.archive());
        }
        issueCursor++;
        issuesProgressBar.update(issueCursor);
    }
    issuesProgressBar.stop();
    console.info(chalk__default["default"].green(`${importer.name} issues imported to your team: https://linear.app/team/${teamKey}/all`));
});
// Build comments into issue description
const buildComments = (client, description, comments, importData) => __awaiter(void 0, void 0, void 0, function* () {
    const newComments = [];
    for (const comment of comments) {
        const user = importData.users[comment.userId];
        const date = comment.createdAt ? comment.createdAt.toISOString().split("T")[0] : undefined;
        const body = yield replaceImagesInMarkdown(client, comment.body || "", importData.resourceURLSuffix);
        newComments.push(`**${user.name}**${" " + date}\n\n${body}\n`);
    }
    return `${description}\n\n---\n\n${newComments.join("\n\n")}`;
});
const createIssueWithRetries = (client, input, retries = 3) => __awaiter(void 0, void 0, void 0, function* () {
    try {
        return yield client.createIssue(input);
    }
    catch (error) {
        if (error.type === "Ratelimited" && retries > 0) {
            // Hard-coded to 1 minute for now; when we do LIN-17685, we can use the X-RateLimit-Endpoint-Requests-Remaining
            // header to find out how long to wait.
            yield new Promise(resolve => setTimeout(resolve, 60000));
            return createIssueWithRetries(client, input, retries - 1);
        }
        else {
            throw error;
        }
    }
});

exports.githubImport = githubImport;
exports.importIssues = importIssues;
//# sourceMappingURL=index-cjs.js.map
