#!/usr/bin/python3

"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.

This file is part of RepoLib.

RepoLib is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

RepoLib 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with RepoLib.  If not, see <https://www.gnu.org/licenses/>.
"""

from pathlib import Path

from ..key import SourceKey
from .. import system, util

from .command import Command

KEYS_PATH = Path(util.KEYS_DIR)

class Key(Command):
    """Key subcommand.
    
    Manages signing keys for repositories, allowing adding, removal, and 
    fetching of remote keys.

    Options:
        --name, -n
        --path, -p
        --url, -u
        --ascii, -a
        --fingerprint, -f
        --keyserver, -s
        --remove, -r
    """

    @classmethod
    def init_options(cls, subparsers) -> None:
        """Sets up this command's options parser"""

        sub = subparsers.add_parser(
            'key',
            help='Manage repository signing keys',
            epilog=(
                'Note that no verification of key validity is performed when '
                'managing key using apt-mange. Ensure that keys are valid to '
                'avoid errors with updates.'
            )
        )

        sub.add_argument(
            'repository',
            default=['x-repolib-none'],
            help='Which repository to manage keys for.'
        )

        sub_group = sub.add_mutually_exclusive_group()

        sub_group.add_argument(
            '-n',
            '--name',
            help='The name of an existing key file to set.'
        )

        sub_group.add_argument(
            '-p',
            '--path',
            help='Sets a path on disk to a signing key.'
        )

        sub_group.add_argument(
            '-u',
            '--url',
            help='Download a key over HTTPS'
        )

        sub_group.add_argument(
            '-a',
            '--ascii',
            help='Add an ascii-armored key'
        )

        sub_group.add_argument(
            '-f',
            '--fingerprint',
            help=(
                'Fetch a key via fingerprint from a keyserver. Use --keyserver '
                'to specify the URL to the keyserver.'
            )
        )

        sub_group.add_argument(
            '-i',
            '--info',
            action='store_true',
            help='Print key information'
        )

        sub_group.add_argument(
            '-r',
            '--remove',
            action='store_true',
            help=(
                'Remove a signing key from the repo. If no other sources use '
                'this key, its file will be deleted.'
            )
        )

        sub.add_argument(
            '-s',
            '--keyserver',
            help=(
                'The keyserver from which to fetch the given fingerprint. '
                '(Default: keyserver.ubuntu.com)'
            )
        )
    
    def finalize_options(self, args):
        super().finalize_options(args)
        self.repo = args.repository
        self.keyserver = args.keyserver

        self.actions:dict = {}
        self.system_source = False

        for act in [
            'name',
            'path',
            'url',
            'ascii',
            'fingerprint',
            'info',
            'remove',
        ]:
            self.actions[act] = getattr(args, act)
        
        system.load_all_sources()
    
    def run(self):
        """Run the command"""
        self.log.info('Modifying signing key settings for %s', self.repo)

        if not self.repo:
            self.log.error('No repository provided')
            return False
        
        try:
            self.source = util.sources[self.repo]
            if self.repo == 'system':
                self.system_source = True
        except KeyError:
            self.log.error(
                'The repository %s was not found. Check the spelling',
                self.repo
            )
            return False
        
        self.log.debug('Actions to take:\n%s', self.actions)
        # Run info, unless a different action is specified
        self.actions['info'] = True
        for key in self.actions:
            if key == 'info':
                self.log.debug('Skipping info key')
                continue
            if self.actions[key]:
                self.log.info('Got an action, skipping info')
                self.actions['info'] = False
                break
         
        self.log.debug('Source before:\n%s', self.source)

        rets = []
        for action in self.actions:
            if self.actions[action]:
                self.log.debug('Running action: %s - (%s)', action, self.actions[action])
                ret = getattr(self, action)(self.actions[action])
                rets.append(ret)
                break
        
        self.log.debug('Results: %s', rets)
        self.log.debug('Source after: \n%s', self.source)

        if self.actions['info']:
            self.log.info('Running Info, skipping saving %s', self.source.ident)
            return True

        if True in rets:
            self.log.info('Saving source %s', self.source.ident)
            self.source.file.save()
            return True
        else:
            self.log.warning('No valid changes specified, no actions taken.')
            return False
        
    def name(self, value:str) -> bool:
        """Sets the key file to a name in the key file directory"""
        if not value:
            return False
        
        key_path = None
        
        for ext in ['.gpg', '.asc']:
            if value.endswith(ext):
                path = KEYS_PATH / value
                if path.exists():
                    key_path = path
                    break
            else:
                if 'archive-keyring' not in value:
                    value += '-archive-keyring'
                path = KEYS_PATH / f'{value}{ext}'
                if path.exists():
                    key_path = path
                    break
        
        if not key_path:
            self.log.error('The key file %s could not be found', value)
            return False
        
        return self.path(str(key_path))
            
    def path(self, value:str) -> bool:
        """Sets the key file to the given path"""
        if not value:
            return False
        
        key_path = Path(value)
        
        if not key_path.exists():
            self.log.error(
                'The path %s does not exist', value
            )
            return False
        
        self.source.signed_by = str(key_path)
        self.source.load_key()
        return True

    def url(self, value:str) -> bool:
        """Downloads a key to use from a provided URL."""
        if not value:
            return False
        
        if not value.startswith('https:'):
            response = 'n'
            self.log.warning(
                'The key is not being downloaded over an encrypted connection, '
                'and may be tampered with in-transit. Only add keys that you '
                'trust, from sources which you trust.'
            )
            response = input('Do you want to continue? (y/N): ')
            if response not in util.true_values:
                return False
        
        key = SourceKey(name=self.source.ident)
        key.load_key_data(url=value)
        self.source.key = key
        self.source.signed_by = str(key.path)
        self.source.load_key()
        return True
    
    def ascii(self, value:str) -> bool:
        """Loads the key from provided ASCII-armored data"""
        if not value:
            return False
        
        response = 'n'
        print('Only add signing keys from data you trust.')
        response = input('Do you want to continue? (y/N): ')

        if response not in util.true_values:
            return False
        
        key = SourceKey(name=self.source.ident)
        key.load_key_data(ascii=value)
        self.source.key = key
        self.source.signed_by = str(key.path)
        self.source.load_key()
        return True
    
    def fingerprint(self, value:str) -> bool:
        """Downloads the key with the given fingerprint from a keyserver"""
        if not value:
            return False
        
        key = SourceKey(name=self.source.ident)
        
        if self.keyserver:
            key.load_key_data(fingerprint=value, keyserver=self.keyserver)
        else:
            key.load_key_data(fingerprint=value)
        
        self.source.key = key
        self.source.signed_by = str(key.path)
        self.source.load_key()
        return True
    
    def info(self, value:str) -> bool:
        """Prints key information"""
        from datetime import date
        if not self.source.key:
            self.log.error(
                'The source %s does not have a key configured.', 
                self.repo
            )
            return True

        else:
            key:dict = self.source.get_key_info()
            output: str = f'Key information for source {self.source.ident}:\n'
            output += f'Key ID: {key["uids"][0]}\n'
            output += f'Fingerprint: {key["keyid"]}\n'
            if key['type'] == 'pub':
                output += 'Key Type: Public\n'
            else:
                output += 'Key Type: Private\n'
            keydate = date.fromtimestamp(int(key['date']))
            output += f'Issue Date: {keydate.ctime()}\n'
            output += f'Length: {key["length"]} Bytes\n'
            output += f'Keyring Path: {str(self.source.key.path)}\n'
            print(output)

        return True
    
    def remove(self, value:str) -> bool:
        """Removes the key from the source"""

        if not self.source.key:
            self.log.error(
                'The source %s does not have a key configured.', 
                self.repo
            )
            return False
        
        response = 'n'
        print(
            'If you remove the key, there may no longer be any verification '
            'of software packages from this repository, including for future '
            'updates. This may cause errors with your updates.'
        )
        response = input('Do you want to continue? (y/N): ')
        if response not in util.true_values:
            return False

        old_key = self.source.key
        self.source.key = None
        self.source.signed_by = ''

        for source in util.sources.values():
            if source.key == old_key:
                self.log.info(
                    'Key file %s in use with another key, not deleting',
                    old_key.path
                )
                return True
        
        response = 'n'
        print('No other sources were found which use this key.')
        response = input('Do you want to remove it? (Y/n): ')
        if response in util.true_values:
            old_key.delete_key()
        
        return True

