#!/usr/bin/env python3

"""
CLI tool for Debian Code Search (https://codesearch.debian.net/)


"""

import argparse
from typing import List, Dict
import requests
import sys
import collections
import html

import debian_codesearch_client.swagger_client as swclient
from debian_codesearch_client.swagger_client.rest import ApiException

configuration = swclient.Configuration()
# API key specifically for https://salsa.debian.org/debian/codesearch-cli,
# please see https://codesearch.debian.net/apikeys/ for details.
configuration.api_key["x-dcs-apikey"] = (
    "MTYxMjczNjE4NnxkTGp0SHdDUnRuUU5WeDh2dGE0dWxtZC1MSFRwQ1o2eUJEdWs1S1VvTjlHYXBYazItUWo3U2daX2pNZ0hHWDVsajJ2TUdhUFNlZ2doU0dIMnFZQk9XNl9zLS1MOU83dTBhNGdnNzV6ZnNDWWVVYXkxVDh0WEJuLTdnUGc9fOH1MJNBLYYnkLVOmnFXFEAkXoHu1SknNqADZuJ2u8nY"
)

dcs = swclient.SearchApi(swclient.ApiClient(configuration))

PATHCOLOR = 33
DUPE_PATHCOLOR = 36

dedupe_results: List[swclient.models.search_result.SearchResult] = []


def say(quiet: bool, msg: str) -> None:
    """Print messages to stderr"""
    if not quiet:
        sys.stderr.write("%s\n" % msg)


def is_excluded(chunk, exclusions: list[str]) -> bool:
    for exclude in exclusions:
        if exclude in chunk.path:
            return True

    return False


def get_result_body(chunk, print_linenum: bool, nocolor: bool) -> str:
    body = ""
    for line in chunk.context_before or ():
        line = html.unescape(line)
        if print_linenum:
            body += "        %s\n" % line
        else:
            body += "%s\n" % line

    line = html.unescape(chunk.context)
    if print_linenum:
        body += "%7d %s\n" % (chunk.line, line)
    else:
        body += "%s\n" % line
        for line in chunk.context_after or ():
            line = html.unescape(line)
            if print_linenum:
                body += "        %s\n" % line
            else:
                body += "%s\n" % line

    return body[:-1]  # trim trailing line


def print_results(
    chunk,
    print_linenum: bool,
    print_only_filenames: bool,
    nocolor: bool
) -> None:
    """Print search results"""
    pathline = "path: %s" % chunk.path
    if nocolor:
        print(pathline)

    else:
        print("\033[%dm%s\033[0m" % (PATHCOLOR, pathline))

    if print_only_filenames:
        return

    print(get_result_body(chunk, print_linenum, nocolor))


def print_dedupe(
    print_linenum: bool,
    print_only_filenames: bool,
    nocolor: bool
) -> None:
    """amalgamate duplicate results and print summary"""

    bodies = collections.defaultdict(list)
    for chunk in dedupe_results:
        body = get_result_body(chunk, print_linenum, nocolor)
        bodies[body].append(chunk)

    for body, chunks in bodies.items():
        print_results(chunks[0], print_linenum, print_only_filenames, nocolor)

        first_path = chunks[0].path
        for chunk in chunks[1:]:
            pathline = "also: %s" % chunk.path
            if nocolor:
                print(pathline)

            else:
                for common_suffix in range(1, len(pathline)):
                    if pathline[-common_suffix:] != first_path[-common_suffix:]:
                        break

                i = 1 - common_suffix
                print(
                    "\033[%dm%s\033[%dm%s\033[0m"
                    % (PATHCOLOR, pathline[:i], DUPE_PATHCOLOR, pathline[i:])
                )
        else:
            print("")


def parse_args() -> argparse.Namespace:
    ap = argparse.ArgumentParser()
    ap.add_argument("searchstring")
    ap.add_argument("--max-results", type=int, default=200)
    ap.add_argument("-q", "--quiet", action="store_true")
    ap.add_argument("-l", "--linenumber", action="store_true")
    ap.add_argument("--nocolor", action="store_true", help="Do not colorize output")
    ap.add_argument(
        "-n",
        "--print-filenames",
        action="store_true",
        help="Print only matching filenames, no contents",
    )
    ap.add_argument(
        "-d",
        "--dedupe",
        action="store_true",
        help="amalgamate results for the same file in different packages",
    )
    ap.add_argument(
        "-x",
        "--exclude",
        action="append",
        help="list of path fragments to exclude from results",
    )
    ap.add_argument(
        "-m",
        "--mode",
        choices=["literal", "regexp"],
        default="regexp",
        help="search mode (literal or regexp)",
    )
    ap.add_argument(
        "--per-package",
        action="store_true",
        default=False,
        help="group results per source package",
    )
    ap.add_argument(
        "--only-package",
        action="store_true",
        default=False,
        help="only print matching source package name",
    )
    args = ap.parse_args()
    if not sys.stdout.isatty():
        args.nocolor = True

    return args


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

    if args.quiet:
        requests.packages.urllib3.disable_warnings()  # type: ignore

    printed_chunks = set()
    try:
        # Searches through source code, see
        # https://codesearch.debian.net/apikeys/#/search/search
        if args.per_package or args.only_package:
            api_response, status, headers = dcs.searchperpackage_with_http_info(
                args.searchstring, match_mode=args.mode, _return_http_data_only=False
            )
        else:
            api_response, status, headers = dcs.search_with_http_info(
                args.searchstring, match_mode=args.mode, _return_http_data_only=False
            )
        printed = 0
        packages = set()
        for chunk in api_response:
            if is_excluded(chunk, args.exclude or []):
                continue
            packages.add(chunk.package)
            if printed == args.max_results:
                break
            printed += 1
            if args.per_package:
                chunk = chunk.results[0]
            if args.only_package:
                continue
            if args.dedupe:
                dedupe_results.append(chunk)
            else:
                print_results(
                    chunk, args.linenumber, args.print_filenames, args.nocolor
                )
            printed_chunks.add((chunk.path, chunk.line))

        if args.only_package:
            if packages:
                print('\n'.join(sorted(packages)))
            return

        if args.dedupe:
            print_dedupe(args.linenumber, args.print_filenames, args.nocolor)

        say(
            args.quiet,
            "--\n%d files found." % int(headers["X-Codesearch-Filestotal"]),
        )

    except ApiException as e:
        print("Exception when calling SearchApi->search: %s\n" % e)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("")
        sys.exit()
