#!/usr/bin/python3

# Copyright © 2009 James Westby <james.westby@ubuntu.com>,
#             2010, 2011 Stefano Rivera <stefanor@ubuntu.com>
#
# ##################################################################
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# ##################################################################

# pylint: disable=invalid-name
# pylint: enable=invalid-name

import argparse
import logging
import re
import sys
import webbrowser
from collections.abc import Iterable
from email.message import EmailMessage

import debianbts
from launchpadlib.launchpad import Launchpad

from ubuntutools import getLogger
from ubuntutools.config import UDTConfig

Logger = getLogger()
ATTACHMENT_MAX_SIZE = 2000


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-b",
        "--browserless",
        action="store_true",
        help="Don't open the bug in the browser at the end",
    )
    parser.add_argument(
        "-l",
        "--lpinstance",
        metavar="INSTANCE",
        help="LP instance to connect to (default: production)",
    )
    parser.add_argument(
        "-v", "--verbose", action="store_true", help="Print info about the bug being imported"
    )
    parser.add_argument(
        "-n",
        "--dry-run",
        action="store_true",
        help="Don't actually open a bug (also sets verbose)",
    )
    parser.add_argument(
        "-p", "--package", help="Launchpad package to file bug against (default: Same as Debian)"
    )
    parser.add_argument(
        "--no-conf", action="store_true", help="Don't read config files or environment variables."
    )
    parser.add_argument("bugs", nargs="+", help="Bug number(s) or URL(s)")
    return parser.parse_args()


def get_bug_numbers(bug_list: Iterable[str]) -> list[int]:
    bug_re = re.compile(r"bug=(\d+)")

    bug_nums = []

    for bug_num in bug_list:
        if bug_num.startswith("http"):
            # bug URL
            match = bug_re.search(bug_num)
            if match is None:
                Logger.error("Can't determine bug number from %s", bug_num)
                sys.exit(1)
            bug_num = match.groups()[0]
        bug_num = bug_num.lstrip("#")
        bug_nums.append(int(bug_num))

    return bug_nums


def walk_multipart_message(message: EmailMessage) -> tuple[str, list[tuple[int, EmailMessage]]]:
    summary = ""
    attachments = []
    i = 1
    for part in message.walk():
        content_type = part.get_content_type()

        if content_type.startswith("multipart/"):
            # we're already iterating on multipart items
            # let's just skip the multipart extra metadata
            continue
        if content_type == "application/pgp-signature":
            # we're not interested in importing pgp signatures
            continue

        if part.is_attachment():
            attachments.append((i, part))
        elif content_type.startswith("image/"):
            # images here are not attachment, they are inline, but Launchpad can't handle that,
            # so let's add them as attachments
            summary += f"Message part #{i}\n"
            summary += f"[inline image '{part.get_filename()}']\n\n"
            attachments.append((i, part))
        elif content_type.startswith("text/html"):
            summary += f"Message part #{i}\n"
            summary += "[inline html]\n\n"
            attachments.append((i, part))
        elif content_type == "text/plain":
            summary += f"Message part #{i}\n"
            summary += part.get_content() + "\n"
        else:
            raise RuntimeError(
                f"""Unknown message part
Your Debian bug is too weird to be imported in Launchpad, sorry.
You can fix that by patching this script in ubuntu-dev-tools.
Faulty message part:
{part}"""
            )
        i += 1

    return summary, attachments


def process_bugs(
    bugs: Iterable[debianbts.Bugreport],
    launchpad: Launchpad,
    package: str,
    dry_run: bool = True,
    browserless: bool = False,
) -> bool:
    debian = launchpad.distributions["debian"]
    ubuntu = launchpad.distributions["ubuntu"]
    lp_debbugs = launchpad.bug_trackers.getByName(name="debbugs")

    err = False
    for bug in bugs:
        ubupackage = package = bug.source
        if package:
            ubupackage = package
        bug_num = bug.bug_num
        subject = bug.subject
        log = debianbts.get_bug_log(bug_num)
        message = log[0]["message"]
        assert isinstance(message, EmailMessage)
        attachments: list[tuple[int, EmailMessage]] = []
        if message.is_multipart():
            summary, attachments = walk_multipart_message(message)
        else:
            summary = str(message.get_payload())

        target = ubuntu.getSourcePackage(name=ubupackage)
        if target is None:
            Logger.error(
                "Source package '%s' is not in Ubuntu. Please specify "
                "the destination source package with --package",
                ubupackage,
            )
            err = True
            continue

        description = f"Imported from Debian bug http://bugs.debian.org/{bug_num}:\n\n{summary}"
        # LP limits descriptions to 50K chars
        description = (description[:49994] + " [...]") if len(description) > 50000 else description

        Logger.debug("Target: %s", target)
        Logger.debug("Subject: %s", subject)
        Logger.debug("Description: ")
        Logger.debug(description)
        for i, attachment in attachments:
            Logger.debug("Attachment #%s (%s)", i, attachment.get_filename() or "inline")
            Logger.debug("Content:")
            if attachment.get_content_type() == "text/plain":
                content = attachment.get_content()
                if len(content) > ATTACHMENT_MAX_SIZE:
                    content = (
                        content[:ATTACHMENT_MAX_SIZE]
                        + f" [attachment cropped after {ATTACHMENT_MAX_SIZE} characters...]"
                    )
                Logger.debug(content)
            else:
                Logger.debug("[data]")

        if dry_run:
            Logger.info("Dry-Run: not creating Ubuntu bug.")
            continue

        u_bug = launchpad.bugs.createBug(target=target, title=subject, description=description)
        for i, attachment in attachments:
            name = f"#{i}-{attachment.get_filename() or "inline"}"
            content = attachment.get_content()
            if isinstance(content, str):
                # Launchpad only wants bytes
                content = content.encode()
            u_bug.addAttachment(
                filename=name,
                data=content,
                comment=f"Imported from Debian bug http://bugs.debian.org/{bug_num}",
            )
        d_sp = debian.getSourcePackage(name=package)
        if d_sp is None and package:
            d_sp = debian.getSourcePackage(name=package)
        d_task = u_bug.addTask(target=d_sp)
        d_watch = u_bug.addWatch(remote_bug=bug_num, bug_tracker=lp_debbugs)
        d_task.bug_watch = d_watch
        d_task.lp_save()
        Logger.info("Opened %s", u_bug.web_link)
        if not browserless:
            webbrowser.open(u_bug.web_link)

    return err


def main() -> None:
    options = parse_args()

    config = UDTConfig(options.no_conf)
    if options.lpinstance is None:
        options.lpinstance = config.get_value("LPINSTANCE")

    if options.dry_run:
        launchpad = Launchpad.login_anonymously("ubuntu-dev-tools")
        options.verbose = True
    else:
        launchpad = Launchpad.login_with("ubuntu-dev-tools", options.lpinstance)

    if options.verbose:
        Logger.setLevel(logging.DEBUG)

    bugs = debianbts.get_status(get_bug_numbers(options.bugs))

    if not bugs:
        Logger.error("Cannot find any of the listed bugs")
        sys.exit(1)

    if process_bugs(bugs, launchpad, options.package, options.dry_run, options.browserless):
        sys.exit(1)


if __name__ == "__main__":
    main()