import * as inquirer from 'inquirer';
import fetch from 'node-fetch';
import { LinearClient } from '@linear/sdk';
import { format } from 'date-fns';
import chalk from 'chalk';
import _, { uniq } from 'lodash';

/******************************************************************************
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")',
    },
];

/* 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 `![${title}](${uploadedUrl})`;
    }));
    // 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, _h, _j, _k, _l, _m, _o, _p;
    const client = new LinearClient({ apiKey });
    const importData = yield importer.import();
    const teamsQuery = yield client.teams();
    const viewerQuery = yield client.viewer;
    const usersQuery = yield client.users();
    const teams = (_a = teamsQuery === null || teamsQuery === void 0 ? void 0 : teamsQuery.nodes) !== null && _a !== void 0 ? _a : [];
    const users = (_c = (_b = usersQuery === null || usersQuery === void 0 ? void 0 : usersQuery.nodes) === null || _b === void 0 ? void 0 : _b.filter(user => user.active)) !== null && _c !== void 0 ? _c : [];
    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 teams.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 _q;
                // 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 = (_q = teamProjects === null || teamProjects === void 0 ? void 0 : teamProjects.nodes) !== null && _q !== void 0 ? _q : [];
                return projects.length > 0;
            }),
        },
        {
            type: "list",
            name: "targetProjectId",
            message: "Import into project:",
            choices: (answers) => __awaiter(void 0, void 0, void 0, function* () {
                var _r;
                // 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 = (_r = teamProjects === null || teamProjects === void 0 ? void 0 : teamProjects.nodes) !== null && _r !== void 0 ? _r : [];
                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 = users.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 = teams === null || teams === void 0 ? void 0 : teams.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 existingLabels = [];
    let teamLabels = yield (teamInfo === null || teamInfo === void 0 ? void 0 : teamInfo.labels());
    existingLabels.push(...((_d = teamLabels === null || teamLabels === void 0 ? void 0 : teamLabels.nodes) !== null && _d !== void 0 ? _d : []));
    while ((_e = teamLabels === null || teamLabels === void 0 ? void 0 : teamLabels.pageInfo) === null || _e === void 0 ? void 0 : _e.hasNextPage) {
        teamLabels = yield teamLabels.fetchNext();
        existingLabels.push(...((_f = teamLabels === null || teamLabels === void 0 ? void 0 : teamLabels.nodes) !== null && _f !== void 0 ? _f : []));
    }
    const organization = yield client.organization;
    let workspaceLabels = yield organization.labels();
    existingLabels.push(...((_g = workspaceLabels === null || workspaceLabels === void 0 ? void 0 : workspaceLabels.nodes) !== null && _g !== void 0 ? _g : []));
    while ((_h = workspaceLabels === null || workspaceLabels === void 0 ? void 0 : workspaceLabels.pageInfo) === null || _h === void 0 ? void 0 : _h.hasNextPage) {
        workspaceLabels = yield workspaceLabels.fetchNext();
        existingLabels.push(...((_j = workspaceLabels === null || workspaceLabels === void 0 ? void 0 : workspaceLabels.nodes) !== null && _j !== void 0 ? _j : []));
    }
    const workflowStates = yield (teamInfo === null || teamInfo === void 0 ? void 0 : teamInfo.states());
    const existingLabelMap = {};
    const existingLabelGroupsMap = {};
    for (const label of existingLabels) {
        const labelName = (_k = label.name) === null || _k === void 0 ? void 0 : _k.toLowerCase();
        const children = yield label.children();
        if (((_l = children === null || children === void 0 ? void 0 : children.nodes) === null || _l === void 0 ? void 0 : _l.length) > 0) {
            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 = _.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 (_m = workflowStates === null || workflowStates === void 0 ? void 0 : workflowStates.nodes) !== null && _m !== void 0 ? _m : []) {
        const stateName = (_o = state.name) === null || _o === void 0 ? void 0 : _o.toLowerCase();
        if (stateName && state.id && !existingStateMap[stateName]) {
            existingStateMap[stateName] = state.id;
        }
    }
    const existingUserMap = {};
    for (const user of users) {
        const userName = (_p = user.name) === null || _p === void 0 ? void 0 : _p.toLowerCase();
        if (userName && user.id && !existingUserMap[userName]) {
            existingUserMap[userName] = user.id;
        }
    }
    // 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 existingAssigneeId = !!issue.assigneeId
            ? existingUserMap[issue.assigneeId.toLowerCase()]
            : 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;
        yield client.createIssue({
            teamId,
            projectId: projectId,
            title: issue.title,
            description,
            priority: issue.priority,
            labelIds,
            stateId,
            assigneeId,
            dueDate: formattedDueDate,
        });
    }
    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")}`;
});

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