import * as inquirer from 'inquirer';
import fetch from 'node-fetch';
import { LinearClient } from '@linear/sdk';
import chalk from 'chalk';
import { SingleBar, Presets } from 'cli-progress';
import { format } from 'date-fns';
import _, { uniq } from 'lodash';
import ora from '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(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.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")',
    },
];

const WORKSPACE_ID = "workspace";
/**
 * Handle importing labels
 *
 * @param client Linear client instance to use for API requests
 * @param importData Import data containing issues and labels
 * @param teamId Team ID being imported to
 * @param existingLabels Existing labels in the team and workspace
 */
const handleLabels = (client, importData, teamId, existingLabels) => __awaiter(void 0, void 0, void 0, function* () {
    const manager = yield LabelManager.create(teamId, existingLabels);
    const labelMapping = {};
    // We process issues instead of labels to validate issue <> label constraints (e.g. only one label from a group)
    for (const issue of importData.issues) {
        const labels = issue.labels;
        if (!labels) {
            continue;
        }
        yield handleIssueLabels(client, manager, teamId, importData, labels, labelMapping);
    }
    return labelMapping;
});
const handleIssueLabels = (client, manager, teamId, importData, issueLabelIds, labelMapping) => __awaiter(void 0, void 0, void 0, function* () {
    var _a, _b;
    let actualLabelId;
    // Track which groups are used to prevent multiple labels from same group
    const usedGroups = new Set();
    for (const labelId of issueLabelIds) {
        const labelData = importData.labels[labelId];
        const parsed = parseLabelName(labelData.name);
        const group = parsed[0];
        let labelName = parsed[1];
        // If this label's group is already used in this issue, or if it was previously mapped as a child label
        // and its group is in use, create/use a root label with the full name.
        if (group && (usedGroups.has(group) || (((_a = labelMapping[labelId]) === null || _a === void 0 ? void 0 : _a.type) === "child" && usedGroups.has(group)))) {
            const fullName = labelData.name;
            const label = manager.getLabelByName(fullName, teamId);
            actualLabelId = label === null || label === void 0 ? void 0 : label.id;
            // If this was previously a child label, delete it before converting to root
            if (((_b = labelMapping[labelId]) === null || _b === void 0 ? void 0 : _b.type) === "child" && !labelMapping[labelId].existedBeforeImport) {
                yield deleteLabel(client, labelMapping[labelId].id);
            }
            if (!actualLabelId) {
                actualLabelId = yield createLabel(client, { name: fullName, teamId, isGroup: false });
                const newRootLabel = new Label(actualLabelId, fullName);
                manager.addLabel({ label: newRootLabel, teamId });
            }
            labelMapping[labelId] = { type: "root", id: actualLabelId, existedBeforeImport: label === null || label === void 0 ? void 0 : label.existedBeforeImport };
            continue;
        }
        // If we already have a mapping for this label and it's not a conflicting child label,
        // use it and track the group
        if (labelMapping[labelId]) {
            if (group) {
                usedGroups.add(group);
            }
            continue;
        }
        let groupLabel = group ? manager.getGroupLabel({ name: group }) : undefined;
        if (group) {
            if (!groupLabel) {
                // Check if the group name conflicts with an existing root label
                const rootLabelConflict = manager.getRootLabel({ name: group });
                if (rootLabelConflict) {
                    // Create the group label with a modified name
                    const groupName = `${group} (imported)`;
                    const groupId = yield createLabel(client, { name: groupName, teamId, isGroup: true });
                    groupLabel = new GroupLabel(groupId, groupName);
                    manager.addLabel({ label: groupLabel, teamId });
                }
                else {
                    // Create new group label
                    const groupId = yield createLabel(client, { name: group, teamId, isGroup: true });
                    groupLabel = new GroupLabel(groupId, group);
                    manager.addLabel({ label: groupLabel, teamId });
                }
            }
            usedGroups.add(group);
        }
        // Handle the child label if we have a valid group
        if (groupLabel) {
            const existingChildLabel = groupLabel.getSubgroupLabel(labelName);
            actualLabelId = existingChildLabel === null || existingChildLabel === void 0 ? void 0 : existingChildLabel.id;
            if (!actualLabelId) {
                // Check for naming conflicts
                const existingLabel = manager.getLabelByName(labelName, teamId);
                const newLabelName = existingLabel ? renameConflictingLabel(labelName) : labelName;
                actualLabelId = yield createLabel(client, {
                    name: newLabelName,
                    parentId: groupLabel.id,
                    teamId,
                    isGroup: false,
                });
                const subgroupLabel = new SubgroupLabel(actualLabelId, newLabelName);
                manager.addLabel({ label: subgroupLabel, parent: groupLabel, teamId });
            }
            labelMapping[labelId] = {
                type: "child",
                id: actualLabelId,
                existedBeforeImport: existingChildLabel === null || existingChildLabel === void 0 ? void 0 : existingChildLabel.existedBeforeImport,
            };
            continue;
        }
        // Handle as root label
        labelName = labelData.name;
        let rootLabelName = labelName;
        // Check for conflicts with existing group labels
        if (manager.getGroupLabel({ name: labelName })) {
            rootLabelName = renameConflictingLabel(labelName);
        }
        // Check for existing root label
        const rootLabel = manager.getRootLabel({ name: rootLabelName });
        actualLabelId = rootLabel === null || rootLabel === void 0 ? void 0 : rootLabel.id;
        if (!actualLabelId) {
            actualLabelId = yield createLabel(client, { name: rootLabelName, teamId, isGroup: false });
            const newRootLabel = new Label(actualLabelId, rootLabelName);
            manager.addLabel({ label: newRootLabel, teamId });
        }
        labelMapping[labelId] = { type: "root", id: actualLabelId, existedBeforeImport: rootLabel === null || rootLabel === void 0 ? void 0 : rootLabel.existedBeforeImport };
    }
});
const renameConflictingLabel = (labelName) => `${labelName} (imported)`;
function parseLabelName(fullName) {
    // Ensure every part is truncated to 80 characters
    const parts = fullName.split("/").map(part => _.truncate(part.trim(), { length: 80 }));
    let group;
    let subgroup;
    if (parts.length > 1) {
        subgroup = parts.pop();
        group = parts.join("/");
    }
    else {
        group = undefined;
        subgroup = fullName;
    }
    return [group, subgroup];
}
class LabelManager {
    constructor(teamId) {
        this.teamId = teamId;
        this.nameToLabel = {};
        this.idToLabel = {};
    }
    /**
     * Create a new label manager.
     *
     * @param teamId The team ID being imported to
     * @param existingLabels Existing labels in the team and workspace
     * @returns LabelManager instance
     */
    static create(teamId, existingLabels) {
        return __awaiter(this, void 0, void 0, function* () {
            const manager = new LabelManager(teamId);
            yield manager.initializeLabels(existingLabels);
            return manager;
        });
    }
    /**
     * Retrieve a label by name in either the workspace or specified team
     *
     * @param name Label name
     * @param teamId Team ID to search in
     * @returns Label instance if found
     */
    getLabelByName(name, teamId) {
        var _a, _b, _c;
        const normalized = Label.normalizeName(name);
        return (_b = (_a = this.nameToLabel[normalized]) === null || _a === void 0 ? void 0 : _a[teamId]) !== null && _b !== void 0 ? _b : (_c = this.nameToLabel[normalized]) === null || _c === void 0 ? void 0 : _c[WORKSPACE_ID];
    }
    /**
     * Retrieve a label by its ID in either the workspace or specified team
     *
     * @param name Label ID
     * @param teamId Team ID to search in
     * @returns Label instance if found
     */
    getLabelById(id, teamId) {
        var _a, _b, _c;
        return (_b = (_a = this.idToLabel[id]) === null || _a === void 0 ? void 0 : _a[teamId]) !== null && _b !== void 0 ? _b : (_c = this.idToLabel[id]) === null || _c === void 0 ? void 0 : _c[WORKSPACE_ID];
    }
    /**
     * Retrieve a group label by name or ID in either the workspace or specified team
     *
     * @param props Object containing either name or ID
     * @returns GroupLabel instance if found
     */
    getGroupLabel(props) {
        return this.getLabel(props, GroupLabel);
    }
    /**
     * Retrieve a root label by name or ID in either the workspace or specified team
     *
     * @param props Object containing either name or ID
     * @returns Label instance if found
     */
    getRootLabel(props) {
        return this.getLabel(props, Label);
    }
    /**
     * Adds a label to the manager
     *
     * @param props Object containing the team ID and label data
     */
    addLabel(props) {
        const { label, teamId = this.teamId } = props;
        this.nameToLabel[label.normalizedName] = { [teamId]: label };
        this.idToLabel[label.id] = { [teamId]: label };
        if ("parent" in props) {
            const { parent } = props;
            parent.addSubgroupLabel(label);
        }
    }
    getLabel(props, instanceType) {
        let label;
        if ("name" in props) {
            label = this.getLabelByName(props.name, this.teamId);
        }
        else {
            label = this.getLabelById(props.id, this.teamId);
        }
        return (label && label instanceof instanceType ? label : undefined);
    }
    initializeLabels(existingLabels) {
        var _a, _b, _c;
        return __awaiter(this, void 0, void 0, function* () {
            // We want to process groups first.
            existingLabels.sort(a => (a.isGroup ? -1 : 1));
            const isExisting = true;
            for (const existingLabel of existingLabels) {
                const labelName = existingLabel.name;
                const teamId = (_b = (_a = (yield existingLabel.team)) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : WORKSPACE_ID;
                if (existingLabel.isGroup && labelName && existingLabel.id) {
                    this.addLabel({ label: new GroupLabel(existingLabel.id, labelName, isExisting), teamId });
                }
                else if (labelName && existingLabel.id) {
                    const parent = yield existingLabel.parent;
                    if (parent) {
                        const group = (_c = this.idToLabel[parent.id]) === null || _c === void 0 ? void 0 : _c[teamId];
                        this.addLabel({ label: new Label(existingLabel.id, labelName, isExisting), parent: group, teamId });
                    }
                    else {
                        this.addLabel({ label: new Label(existingLabel.id, labelName, isExisting), teamId });
                    }
                }
            }
        });
    }
}
const createLabel = (client, { name, isGroup, description, color, parentId, teamId, }) => __awaiter(void 0, void 0, void 0, function* () {
    try {
        const response = yield client.createIssueLabel({ name, description, color, teamId, parentId, isGroup });
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return (yield (response === null || response === void 0 ? void 0 : response.issueLabel)).id;
    }
    catch (_c) {
        // If the label failed to create it's likely it's a name conflict, in which case we try one more time with a new name
        const newName = renameConflictingLabel(name);
        const response = yield client.createIssueLabel({ name: newName, description, color, teamId, parentId, isGroup });
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return (yield (response === null || response === void 0 ? void 0 : response.issueLabel)).id;
    }
});
const deleteLabel = (client, labelId) => __awaiter(void 0, void 0, void 0, function* () {
    yield client.deleteIssueLabel(labelId);
});
/** A root label */
class Label {
    /**
     * Create a new label
     *
     * @param id Label ID returned from the API
     * @param name Label name as it was imported
     * @param existedBeforeImport Whether the label existed before the import
     */
    constructor(id, name, existedBeforeImport = false) {
        this.id = id;
        this.name = name;
        this.existedBeforeImport = existedBeforeImport;
    }
    get normalizedName() {
        return Label.normalizeName(this.name);
    }
    static normalizeName(name) {
        // Trim and lowercase to prevent duplicates
        return name.toLowerCase().trim();
    }
}
/** A label group (parent label) */
class GroupLabel extends Label {
    constructor(id, name, existedBeforeImport) {
        super(id, name, existedBeforeImport);
        this.labels = {};
    }
    addSubgroupLabel(label) {
        this.labels[label.normalizedName] = label;
    }
    getSubgroupLabel(name) {
        return this.labels[Label.normalizeName(name)];
    }
}
/** A label in a group (child label)  */
class SubgroupLabel extends Label {
}

/* 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;
});

var IssueStatus;
(function (IssueStatus) {
    IssueStatus["Backlog"] = "backlog";
    IssueStatus["Started"] = "started";
    IssueStatus["Completed"] = "completed";
})(IssueStatus || (IssueStatus = {}));
const defaultStateColors = {
    [IssueStatus.Backlog]: "#bec2c8",
    [IssueStatus.Started]: "#f2c94c",
    [IssueStatus.Completed]: "#5e6ad2",
};
/**
 * Import issues into Linear via the API.
 */
const importIssues = (apiKey, importer, apiUrl) => __awaiter(void 0, void 0, void 0, function* () {
    var _a, _b, _c, _d, _e, _f;
    const client = new LinearClient({ apiKey, apiUrl });
    const importData = yield importer.import();
    const viewerQuery = yield client.viewer;
    let spinner = ora("Fetching teams and users").start();
    const allTeams = yield client.paginate(client.teams, {});
    allTeams.sort((a, b) => a.displayName.localeCompare(b.displayName));
    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.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.displayName}`,
                    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 _g;
                // 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 = (_g = teamProjects === null || teamProjects === void 0 ? void 0 : teamProjects.nodes) !== null && _g !== void 0 ? _g : [];
                return projects.length > 0;
            }),
        },
        {
            type: "list",
            name: "targetProjectId",
            message: "Import into project:",
            choices: (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.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;
    spinner = ora("Fetching labels").start();
    const allTeamLabels = yield teamInfo.paginate(teamInfo.labels, {});
    const allWorkspaceLabels = yield client.paginate(organization.labels, {});
    spinner.stop();
    spinner = ora("Fetching workflow states").start();
    const workflowStates = yield (teamInfo === null || teamInfo === void 0 ? void 0 : teamInfo.states());
    const projectId = importAnswers.targetProjectId;
    const labelMapping = yield handleLabels(client, importData, teamId, [...allTeamLabels, ...allWorkspaceLabels]);
    const existingStateMap = {};
    for (const state of (_a = workflowStates === null || workflowStates === void 0 ? void 0 : workflowStates.nodes) !== null && _a !== void 0 ? _a : []) {
        const stateName = (_b = state.name) === null || _b === void 0 ? void 0 : _b.toLowerCase();
        if (stateName && state.id && !existingStateMap[stateName]) {
            existingStateMap[stateName] = state.id;
        }
    }
    const existingUserMapByName = {};
    const existingUserMapByEmail = {};
    for (const user of allUsers) {
        const userName = (_c = user.name) === null || _c === void 0 ? void 0 : _c.toLowerCase();
        if (userName && !existingUserMapByName[userName]) {
            existingUserMapByName[userName] = user.id;
        }
        if (!existingUserMapByEmail[user.email]) {
            existingUserMapByEmail[user.email] = user.id;
        }
    }
    spinner.stop();
    const issuesProgressBar = new SingleBar({}, 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].id)) : 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 = IssueStatus.Backlog;
            if (issue.completedAt) {
                stateType = IssueStatus.Completed;
            }
            else if (issue.startedAt) {
                stateType = IssueStatus.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 = (_d = issue.assigneeId) === null || _d === void 0 ? void 0 : _d.toLowerCase();
        const existingAssigneeId = !!issueAssigneeId
            ? ((_e = existingUserMapByEmail[issueAssigneeId]) !== null && _e !== void 0 ? _e : 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 ? format(issue.dueDate, "yyyy-MM-dd") : undefined;
        try {
            const createdIssue = yield createIssueWithRetries(client, {
                teamId,
                projectId: projectId,
                title: issue.title,
                description,
                priority: issue.priority,
                labelIds,
                stateId,
                assigneeId,
                createdAt: issue.createdAt,
                completedAt: issue.completedAt,
                dueDate: formattedDueDate,
                estimate: issue.estimate,
            });
            if (issue.archived) {
                yield ((_f = (yield createdIssue.issue)) === null || _f === void 0 ? void 0 : _f.archive());
            }
            issueCursor++;
            issuesProgressBar.update(issueCursor);
        }
        catch (error) {
            issuesProgressBar.stop();
            throw error;
        }
    }
    issuesProgressBar.stop();
    console.info(chalk.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;
        }
    }
});

export { githubImport, importIssues };
//# sourceMappingURL=index-es.js.map
