#!/usr/bin/env python3
import csv
import logging
import os
import re
from argparse import ArgumentParser

import gnupg

logger = logging.getLogger(__name__)


class CSVExporter:
    def __init__(self, kpx_format, login_fields, get_url, exclude_rows):
        logging.basicConfig(level=logging.INFO)
        self.logger = logger

        # Set to True to allow for alternate password csv to be created
        # See README for differences
        self.kpx_format = kpx_format

        if self.kpx_format:
            # A list of possible fields (in order) that could be converted to
            # login fields
            self.login_fields = login_fields or []
            # Set to True to extract url fields
            self.get_url = get_url
            # A regular expression list of lines that should be excluded from
            # the notes field
            self.exclude_rows = exclude_rows or []

        self.logger.info("Using KPX format: %s", self.kpx_format)

    def traverse(self, path):
        for root, dirs, files in os.walk(path):
            if '.git' in dirs:
                dirs.remove('.git')
            for name in files:
                yield os.path.join(root, name)

    def get_metadata(self, notes_raw):
        lines = notes_raw.split('\n')

        # A list of lines to keep as notes (will be joined by newline)
        notes = []
        # The extracted user field
        user = ''
        # The extracted URL field
        url = ''

        # This will extract each field name (for example, if a line in notes
        # was `user: user1`, fields should contain 'user')
        all_fields = set()
        for line in lines:
            field_search = re.search('^(.*) ?: ?.*$', line, re.I)
            if field_search:
                all_fields.add(field_search.group(1))

        # Check if any of the fields match the login names
        login_fields = [
            field for field in self.login_fields if field in all_fields
        ]
        # Get the field to use for the login. Since self.login_fields is in order,
        # the 0th element will contain the first match
        login_field = None if not login_fields else login_fields[0]

        # Iterate through the file again to build the return array
        for line in lines:
            # If any of the exclusion patterns match, ignore the line
            if [pattern for pattern in self.exclude_rows if re.search(pattern, line, re.I)]:
                continue

            if login_field:
                user_search = re.search(
                    '^' + login_field + ' ?: ?(.*)$', line, re.I)
                if user_search:
                    user = user_search.group(1)
                    # The user was matched, don't add it to notes
                    continue

            if self.get_url:
                url_search = re.search('^url ?: ?(.*)$', line, re.I)
                if url_search:
                    url = url_search.group(1)
                    # The url was matched, don't add it to notes
                    continue

            notes.append(line)

        return (user, url, '\n'.join(notes).strip())

    def parse(self, basepath, path, data):
        name = os.path.splitext(os.path.basename(path))[0]
        group = os.path.dirname(os.path.os.path.relpath(path, basepath))
        split_data = data.split('\n', maxsplit=1)
        password = split_data[0]
        # Perform if/else in case there are no notes for a field
        notes = split_data[1] if len(split_data) > 1 else ""
        self.logger.info("Processing %s", name)
        if self.kpx_format:
            # We are using the advanced format; try extracting user and url
            user, url, notes = self.get_metadata(notes)
            return [group, name, user, password, url, notes]
        else:
            # We are not using KPX format; just use notes
            return [group, name, password, notes]


def main(gpgbinary, use_agent, pass_path,
         kpx_format, login_fields, get_url, exclude_rows):
    exporter = CSVExporter(kpx_format, login_fields, get_url, exclude_rows)
    gpg = gnupg.GPG(use_agent=use_agent, gpgbinary=gpgbinary)
    gpg.encoding = 'utf-8'
    csv_data = []
    for file_path in exporter.traverse(pass_path):
        if os.path.splitext(file_path)[1] == '.gpg':
            with open(file_path, 'rb') as f:
                data = str(gpg.decrypt_file(f))
                if len(data) == 0:
                    logger.warning("Could not decrypt %s or it is empty.", file_path)
                csv_data.append(exporter.parse(pass_path, file_path, data))

    with open('pass.csv', 'w', newline='') as csv_file:
        writer = csv.writer(csv_file, delimiter=',')
        writer.writerows(csv_data)


class OptionsParser(ArgumentParser):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.add_argument(
            'pass_path',
            metavar='path',
            type=str,
            help="path to the password-store folder to export",
        )

        self.add_argument(
            '-a', '--agent',
            action='store_true',
            help="ask gpg to use its auth agent",
            dest='use_agent',
        )

        self.add_argument(
            '-b', '--gpgbinary',
            type=str,
            help="path to the gpg binary you wish to use",
            dest='gpgbinary',
            default="gpg"
        )

        self.add_argument(
            '-x', '--kpx',
            action='store_true',
            help="format the CSV for KeePassXC",
            dest='kpx_format',
        )

        self.add_argument(
            '-l', '--login-fields',
            action='extend',
            nargs='+',
            type=str,
            help="strings to interpret as names of login fields (only used with -x)"
        )

        self.add_argument(
            '-u', '--get-url',
            action='store_true',
            help="match row starting with 'url:' and extract it (only used with -x)"
        )

        self.add_argument(
            '-e', '--exclude-rows',
            action='extend',
            nargs='+',
            type=str,
            help="regexps to exclude from the notes field (only used with -x)"
        )


if __name__ == '__main__':
    PARSER = OptionsParser()
    ARGS = PARSER.parse_args()
    main(**vars(ARGS))
