import json
import os
import tempfile
from argparse import ArgumentParser
from xml.etree import ElementTree

import requests

from conda.api import PrefixData, Solver
from conda.base.constants import DEFAULTS_CHANNEL_NAME
from conda.base.context import context
from conda.cli.common import disp_features
from conda.cli.conda_argparse import add_parser_networking, add_parser_prefix
from conda.gateways.connection.session import CONDA_SESSION_SCHEMES
from conda_env import specs
from conda_env.cli.common import get_prefix
from repo_cli.utils import file_or_token
from repo_cli.utils.api import RepoApi
from repo_cli.utils.config import get_config, load_token

try:
    from conda_env import exceptions
except (ModuleNotFoundError, ImportError):
    from conda import exceptions


COLOR_RED = "\u001B[31m{}\u001B[0m"
COLOR_BLUE = "\u001B[34m{}\u001B[0m"
COLOR_YELLOW = "\u001B[33m{}\u001B[0m"
COLOR_CYAN = "\u001B[36m{}\u001B[0m"
COLOR_GREEN = "\u001B[32m{}\u001B[0m"

MAX_LINE_WIDTH = 80


def expand(path):
    return os.path.abspath(os.path.expanduser(os.path.expandvars(path)))

def repo_url_headers(site:str = None, token=None):
    if site is not None and site.startswith('http'):
        url = site
    else:
        all_sites = get_config()
        if site is None:
            site = all_sites.get('default_site')

        site_config = all_sites.get('sites', {}).get(site, {})
        if not site_config or ('url' not in site_config):
            raise RuntimeError(f"""The Site {site} has not been properly configured. Please run conda repo config to configure the site.""")

        url = site_config.get('url')

    if token is None:
        token = load_token(site)
        if token is None:
            raise RuntimeError(f"""You must run conda repo -s {site} login first.
Or you can supply a token here with conda audit -s {site} -t <TOKEN>.""")
    api = RepoApi(url, token, verify_ssl=True)

    return url, api.xauth_headers


def strike(text):
    return ''.join([u'\u0336{}'.format(c) for c in text])


def bold(text):
    return ''.join([u'\u001b[1m{}'.format(c) for c in text])


def underline(text):
    return ''.join([u'\u0332{}'.format(c) for c in text])


def installed_packages(prefix):
    pd = PrefixData(prefix)
    return sorted(pd.iter_records(), key=lambda x:x.name)


def _ckey(record):
    return f'{record.subdir}/{record.fn}'


def _purl(record):
    return f'pkg:anaconda/{record.subdir}/{record.name}@{record.version}-{record.build}'


def _child(key, value, tags=None):
    if tags:
        child = ElementTree.Element(key, tags)
    else:
        child = ElementTree.Element(key)

    child.text = value
    return child


def get_cves(records, url, headers):
    r = requests.post(f'{url}/cves-report',
                      data=json.dumps({
                          'sha256s': [r.sha256 for r in records]
                      }),
                      headers=headers)
    r.raise_for_status()
    cves = r.json()['items']
    return cves


def packages_and_cves(prefix=None, solved_pkgs=None, site=None, token=None):
    if prefix is not None:
        packages = installed_packages(prefix)
    elif solved_pkgs is not None:
        packages = solved_pkgs

    url, headers = repo_url_headers(site, token)

    cves = get_cves(packages, url, headers)

    package_info = []
    for package, cves in zip(packages, cves):
        assert package.sha256 == cves['sha256'], f'Something went wrong with the CVE retrieval for {package.name}'
        package.cves = cves['cves']

        package_info.append(package)
    return package_info


def build_sbom(packages_and_cves):
    namespace = {'xmlns': 'http://cyclonedx.org/schema/bom/1.2',
                 'xmlns:v': 'http://cyclonedx.org/schema/ext/vulnerability/1.0',
                 'version': '1'}
    sbom = ElementTree.Element("bom", namespace)

    components = ElementTree.SubElement(sbom, "components")

    for package in packages_and_cves:
        library = ElementTree.Element('component',
                                      {"type": "library",
                                       "bom-ref": _purl(package)})

        library.append(_child('publisher', 'Anaconda'))
        library.append(_child('purl', _purl(package)))

        vulnerabilities = ElementTree.SubElement(library, 'v:vulnerabilities')

        for cve in package.cves:
            if cve['status'] not in ['active', 'reported']:
                continue

            vulnerability = ElementTree.Element('v:vulnerability', {'ref': _purl(package)})
            vulnerability.append(_child('v:id', cve['id']))

            source = ElementTree.SubElement(vulnerability, 'v:source', {'name':'NVD'})
            source.append(_child('v:url', f'https://nvd.nist.gov/vuln/detail/{cve["id"]}'))

            ratings = ElementTree.SubElement(vulnerability, 'v:ratings')

            rating = ElementTree.SubElement(ratings, 'v:rating')
            scores = ElementTree.SubElement(rating, 'v:score')

            scores.append(_child('v:base', cve['cvssv2']['cvssV2']['baseScore']))
            scores.append(_child('v:impact', cve['cvssv2']['impactScore']))
            scores.append(_child('v:exploitability', cve['cvssv2']['exploitabilityScore']))

            rating.append(_child('v:severity', cve['cvssv2']['severity'].capitalize()))
            rating.append(_child('v:method', 'CVSSv2'))
            rating.append(_child('v:vector', cve['cvssv2']['cvssV2']['vectorString']))

            vulnerability.append(_child('v:description', cve['description']))

            vulnerabilities.append(vulnerability)

        components.append(library)

    return sbom


# CVE severity ratings defined here
# https://nvd.nist.gov/vuln-metrics/cvss

def cve_color_v2(cve_score):
    color = '{}'
    if cve_score >= 7.0:
        color = COLOR_RED
    elif cve_score >= 4.0:
        color = COLOR_YELLOW
    else:
        color = COLOR_GREEN

    return color


def cve_color_v3(cve_score):
    color = '{}'
    if cve_score >= 9.0:
        color = COLOR_RED
    elif cve_score >= 7.0:
        color = COLOR_YELLOW
    elif cve_score >= 4.0:
        color = COLOR_CYAN
    elif cve_score >= 0.1:
        color = COLOR_GREEN

    return color


def generate_json_report(packages_and_cves):
    report = []
    for pkg in packages_and_cves:
        package = {
            "base_url": pkg.channel.base_url,
            "build_number": pkg.build_number,
            "build_string": pkg.build,
            "channel": pkg.channel.name,
            "dist_name": pkg.dist_str().rsplit(':', 1)[-1],
            "name": pkg.name,
            "platform": pkg.subdir,
            "version": pkg.version,
            "cves": pkg.cves
        }
        report.append(package)
    print(json.dumps(report, indent=2))



def generate_audit(packages_and_cves, prefix, show_channel_urls=None):
    print('# packages in environment at {:s}:'.format(prefix))
    print('#')
    print('# %-23s %-15s %15s Channel' % ("Name", "Version", "Build"))
    for package in packages_and_cves:
        # borrowed from conda_list
        disp = '%(name)-25s %(version)-15s %(build)25s' % package  # NOQA lgtm [py/percent-format/wrong-arguments]
        features = set(package.get('features') or ())
        disp += '  %s' % disp_features(features)
        schannel = package.get('schannel')
        show_channel_urls = show_channel_urls or context.show_channel_urls
        if (show_channel_urls or show_channel_urls is None
                and schannel != DEFAULTS_CHANNEL_NAME):
            disp += '  %s' % schannel
        print(disp)

        line_width = 4
        if package.cves:
            print(' -- ', end='')
            for cve in sorted(package.cves, key=lambda x: x['score']):
                cve_id = format_cve_id(cve)

                cve_id_score = '{} ({})'.format(cve['id'], cve['score'])
                line_width += len(cve_id_score) + 2

                if line_width > MAX_LINE_WIDTH:
                    print()
                    print(' -- ', end='')
                    line_width = 4

                print('{} '.format(cve_id), end='')
            print()


def format_cve_id(cve):
    cve_id_score = '{} ({})'.format(cve['id'], cve['score'])

    if cve['status'] in ['mitigated', 'cleared']:
        cve_id_score = strike(cve_id_score)
    elif cve['status'] == 'disputed':
        cve_id_score = underline(cve_id_score)

    id_ = bold('✔' + cve_id_score) if cve['curated'] else cve_id_score

    # if cve.get('cvssv3_score', False):
    #     cve_id = cve_color_v3(cve['cvssv3_score']).format(id_)
    # else:
    #     cve_id = cve_color_v2(cve['cvssv2_score']).format(id_)
    cve_id = cve_color_v3(cve['score']).format(id_)

    return cve_id


def html_cve_color_v2(cve_score):
    color = '{}'
    if cve_score >= 7.0:
        color = '#A93226;'
    elif cve_score >= 4.0:
        color = '#D4AC0D;'
    else:
        color = '#29954;'

    return color


def html_cve_color_v3(cve_score):
    color = '{}'
    if cve_score >= 9.0:
        color = '#A93226;'
    elif cve_score >= 7.0:
        color = '#D4AC0D;'
    elif cve_score >= 4.0:
        color = '#28B463;'
    elif cve_score >= 0.1:
        color = '#229954;'

    return color


def html_format_cve_id(cve):
    cve_id_score = '{} ({})'.format(cve['id'], cve['score'])

    if cve['status'] in ['mitigated', 'cleared']:
        cve_id_score = f'<strike>{cve_id_score}</strike>'
    elif cve['status'] == 'disputed':
        cve_id_score = f'<u>{cve_id_score}</u>'

    id_ = '<b>✔' + cve_id_score + '</b>' if cve['curated'] else cve_id_score

    if cve.get('cvssv3_score', False):
        color = html_cve_color_v3(cve['cvssv3_score'])
    else:
        color = html_cve_color_v2(cve['cvssv2_score'])

    link = f"""<a href=https://nvd.nist.gov/vuln/detail/{cve["id"]} target="_blank" style="color:{color}">{id_}</a>"""

    return link


def cli():
    parser = ArgumentParser('conda-audit')
    # parser.add_argument('--sbom', action='store_true',
    #                     help='Generate Software Bill of Materials (SBOM) xml file.')
    parser.add_argument('-f', '--file', action='store',
                        help='environment definition file')
    parser.add_argument('-t', '--token', type=file_or_token,
                        help='Authentication token to use. May be a token or a path to a file'
                        ' containing a token')
    parser.add_argument('-s', '--site', help='Select the repository site to use. You can supply the site name '
                                             'configured using conda-repo-cli or the full URL.'
                        )
    parser.add_argument('--json', action='store_true',
                        help="Output audit in JSON format.")
    parser.add_argument(
        'remote_definition',
        help='remote environment definition / IPython notebook',
        action='store',
        default=None,
        nargs='?'
    )

    add_parser_prefix(parser)
    add_parser_networking(parser)

    args = parser.parse_args()

    if args.file or args.remote_definition:
        name = args.remote_definition or args.name
        try:
            if args.file:
                url_scheme = args.file.split("://", 1)[0]
                if url_scheme in CONDA_SESSION_SCHEMES:
                    filename = args.file
                else:
                    filename = expand(args.file)
            else:
                filename = expand('environment.yml')

            spec = specs.detect(name=name, filename=filename, directory=os.getcwd())
            env = spec.environment
            solver = Solver(tempfile.mkdtemp(), channels=env.channels,
                            specs_to_add=env.dependencies['conda'])
            solved = solver.solve_final_state()
            pkgs = packages_and_cves(solved_pkgs=solved, site=args.site, token=args.token)

            # FIXME conda code currently requires args to have a name or prefix
            # don't overwrite name if it's given. gh-254
            if args.prefix is None and args.name is None:
                args.name = env.name

            prefix = args.remote_definition or filename

        except exceptions.SpecNotFound:
            raise
    else:
        prefix = get_prefix(args, search=False)
        pkgs = packages_and_cves(prefix=prefix, site=args.site, token=args.token)

    # if args.sbom:
    #     sbom = build_sbom(pkgs)

    #     from xml.dom import minidom
    #     as_string = ElementTree.tostring(sbom,
    #                                     encoding='UTF-8',
    #                                     method='xml').decode()
    #     print(minidom.parseString(as_string).toprettyxml())
    # else:
    #     generate_audit(pkgs, prefix)
    if args.json:
        generate_json_report(pkgs)
    else:
        generate_audit(pkgs, prefix)


if __name__ == '__main__':
    cli()
