# pylint: disable=duplicate-code,protected-access

import copy
import gzip
import os
import tempfile
import shutil
import sys

import yaml

from apt_listchanges.ALCConfig import ALCConfig
from apt_listchanges.ALCSeenDb import SeenDb
from apt_listchanges.DebianFiles import (
    ChangelogsReader,
    ControlParser,
    Package,
)
from apt_listchanges.apt_listchanges import Filterer

dpkg_status_file = '/var/lib/dpkg/status'


def normalize_package_data(data):
    data = copy.deepcopy(data)
    pkgdata = data['pkgdata']
    name = pkgdata['name']
    pkgdata.setdefault('source', name)
    pkgdata.setdefault('source_version', pkgdata['version'])
    files = data.setdefault('files', {})
    for filename in list(files.keys()):
        if filename.startswith('/'):
            continue
        if '/' in filename:
            new_filename = os.path.join('/usr/share/doc', filename)
        else:
            new_filename = os.path.join('/usr/share/doc', name, filename)
        files[new_filename] = files.pop(filename)
    return data


class DataPackage(Package):
    # pylint: disable=super-init-not-called
    def __init__(self, package_data, filterer):
        package_data = normalize_package_data(package_data)
        pkgdata = package_data['pkgdata']
        self.binary = pkgdata['name']
        self.source = pkgdata['source']
        self.source_version = pkgdata['source_version']
        self.version = pkgdata['version']
        self.arch = pkgdata['arch']
        self.files = package_data.get('files', {})
        self.apt_changelog = package_data.get('apt_changelog', '')
        self.path = package_data.get('path', '<in-memory package>')
        self.filterer = filterer

    def _extract_contents(self, filenames):
        temp_dir = tempfile.mkdtemp(prefix='apt-listchanges')
        for name, contents in self.files.items():
            target_path = os.path.join(temp_dir, name[1:])
            os.makedirs(os.path.dirname(target_path), exist_ok=True)
            if name.endswith('.gz'):
                file_constructor = gzip.open
            else:
                file_constructor = open
            with file_constructor(target_path, 'wt', encoding='utf-8') as f:
                f.write(contents)
        return temp_dir

    def _extract_filenames(self, patterns):
        all_files = [f[1:] for f in self.files.keys()]
        return self.fn_pattern_match(all_files, patterns)

    def _extract_filenames_via_installed(self, patterns):
        return self._extract_filenames(patterns)

    # I would prefer to test extract_via_apt by using pytest-subprocess, but
    # that is currently not available in Debian (see bug#1053360), so the best
    # we can do for the time being is stub out the whole function.
    def extract_changes_via_apt(self, filterer=None, installed=None):
        # Retrieve changelog file and save it in a temporary directory
        tempdir = tempfile.mkdtemp(prefix='apt-listchanges')
        changelog_file = f'network/{self.binary}'
        try:
            os.mkdir(os.path.join(tempdir, os.path.dirname(changelog_file)))
            with open(os.path.join(tempdir, changelog_file), 'wb') as \
                 changelog_fd:
                if self.apt_changelog:
                    changelog_fd.write(self.apt_changelog.encode())
                    changelog_fd.flush()
                return ChangelogsReader(
                    self, tempdir, filterer or self.filterer,
                    installed=installed).read_changelogs(
                        [changelog_file], None, version_filtering=True)
        finally:
            changelog_fd.close()
            shutil.rmtree(tempdir, True)

        return None


# Not using this at the moment but leaving it in in case we need it later.
def yaml_str_representer(dumper, data):
    """configures yaml for dumping multiline strings
    Ref: https://stackoverflow.com/questions/8640959/how-can-i-control-what-
    scalar-form-pyyaml-uses-for-my-data"""
    if data.count('\n') > 0:  # check for multiline string
        return dumper.represent_scalar(
            'tag:yaml.org,2002:str', data, style='|')
    return dumper.represent_scalar('tag:yaml.org,2002:str', data)


yaml.add_representer(str, yaml_str_representer)


def make_config(data=None):
    data = data or {}
    config = ALCConfig()
    for key, value in data.items():
        setattr(config, key, value)
    keys = list(data.keys())
    if 'debug' not in keys:
        config.debug = True
    if 'no_network' not in keys:
        config.no_network = True
    return config


def make_dpkg_status(filename_or_content):
    status = ControlParser()
    if filename_or_content:
        filename_or_content = find_test_file(filename_or_content)
        if os.path.exists(filename_or_content):
            status.readfile(filename_or_content)
        else:
            with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8') as f:
                f.write(filename_or_content)
                f.flush()
                status.readfile(f.name)
    status.makeindex('Package')
    return status


def setup_filterer(filterer=None, seen_db=None, config=None, latest=None):
    if not filterer:
        if seen_db is None:
            seen_db = SeenDb()
        if config is None:
            config = make_config()
        filterer = Filterer(config, seen_db, latest=latest)
    return filterer


def package_from_yaml_file(filename, filterer=None, seen_db=None, config=None):
    filterer = setup_filterer(filterer, seen_db, config)
    path = find_test_file(filename)
    with open(path, 'r', encoding='utf-8') as f:
        package_data = yaml.load(f, Loader=yaml.Loader)
    package_data['path'] = path
    return DataPackage(package_data, filterer)


def package_from_deb_file(filename, filterer=None, seen_db=None, config=None):
    filterer = setup_filterer(filterer, seen_db, config)
    path = find_test_file(filename)
    return Package(path, filterer)


def install_yaml_package(fs, filename):
    filename = find_test_file(filename)
    with open(filename, 'r', encoding='utf-8') as f:
        data = yaml.load(f, Loader=yaml.Loader)
    data = normalize_package_data(data)
    install_dpkg_status(fs, data)
    install_files(fs, data)


def install_dpkg_status(fs, data):
    if not os.path.exists(dpkg_status_file):
        fs.create_file(dpkg_status_file)
    pkgdata = data['pkgdata']
    with open(dpkg_status_file, 'at', encoding='utf-8') as f:
        print(f'Package: {pkgdata["name"]}', file=f)
        print(f'Version: {pkgdata["version"]}', file=f)
        if pkgdata['name'] == pkgdata['source']:
            source = pkgdata['name']
        else:
            source = pkgdata['source']
            if pkgdata['version'] != pkgdata['source_version']:
                source = f'{source} ({pkgdata["source_version"]})'
        print(f'Source: {source}', file=f)
        print(f'Architecture: {pkgdata["arch"]}', file=f)
        print('Status: install ok installed', file=f)
        print('', file=f)


def install_files(fs, data):
    files = data['files']
    for path, contents in files.items():
        if not os.path.exists(path):
            fs.create_file(path)
        if path.endswith('.gz'):
            opener = gzip.open
        else:
            opener = open
        with opener(path, 'wt', encoding='utf-8') as f:
            f.write(contents)


def deb_to_yaml(filename, outfile=sys.stdout, via_apt=True):
    filename = find_test_file(filename)
    pkg = Package(filename, setup_filterer())
    data = {
        'pkgdata': {
            'name': pkg.binary,
            'version': pkg.version,
            'source': pkg.source,
            'source_version': pkg.source_version,
            'arch': pkg.arch,
        },
        'files': {},
    }
    patterns = pkg.news_filenames + pkg.changelog_filenames + \
        pkg.binnmu_filenames
    tempdir = pkg._extract_contents(patterns)
    try:
        for dirpath, dirnames, dirfiles in os.walk(tempdir):
            for dirfile in dirfiles:
                if dirfile.endswith('.gz'):
                    opener = gzip.open
                else:
                    opener = open
                fullpath = os.path.join(dirpath, dirfile)
                pkgpath = fullpath[len(tempdir):]
                try:
                    with opener(fullpath, 'rt', encoding='utf-8') as f:
                        data['files'][pkgpath] = f.read()
                except FileNotFoundError:
                    print(f'WARNING: Could not open {pkgpath}, '
                          f'probably a symlink', file=sys.stderr)
        if via_apt:
            if apt_file := pkg.extract_content_via_apt(tempdir):
                with open(os.path.join(tempdir, apt_file), 'rt',
                          encoding='utf-8') as f:
                    data['apt_changelog'] = f.read()
    finally:
        shutil.rmtree(tempdir, True)
    yaml.dump(data, outfile, Dumper=yaml.Dumper, sort_keys=False)


def find_test_file(filename):
    if os.path.exists(filename):
        return filename
    indir = os.path.dirname(__file__)
    path = os.path.join(indir, filename)
    if os.path.exists(path):
        return path
    indir = os.path.join(indir, 'package_files')
    path = os.path.join(indir, filename)
    if os.path.exists(path):
        return path
    return filename
