import sys
from datetime import datetime
from argparse import ArgumentParser
from distutils.spawn import find_executable
from os.path import isfile

from stegcracker import __url__, __version__, __description__
from stegcracker.cracker import Cracker
from stegcracker.helpers import error, wc, handle_interrupt, DevNull, log, CustomHelpFormatter


DEFAULT_WORDLIST_PATH = '/usr/share/wordlists/rockyou.txt'


@handle_interrupt
def main():
    """Main entry point of the application"""

    if any(i in sys.argv for i in ('-q', '--quiet', '--stfu')):
        if any(i in sys.argv for i in ('-V', '--verbose')):
            return error(
                "Arguments 'verbose' and 'quiet', are mutually exclusive. "
                "You have to choose either one of them, not both.")

        sys.stderr = DevNull()

    if any(i in sys.argv for i in ('-v', '--version')):
        return print(__version__)

    log(f'StegCracker {__version__} - ({__url__})',)
    log(f'Copyright (c) {datetime.now().year} - Luke Paris (Paradoxis)')
    log('')
    log('StegCracker has been retired following the release of StegSeek, which ')
    log('will blast through the rockyou.txt wordlist within 1.9 second as opposed ')
    log('to StegCracker which takes ~5 hours.')
    log('')
    log('StegSeek can be found at: https://github.com/RickdeJager/stegseek')
    log('')

    args = ArgumentParser(
        usage='stegcracker <file> [<wordlist>]',
        formatter_class=CustomHelpFormatter,
        description=__description__)

    args.add_argument('file', action='store', help=(
        f'Input file you think contains hidden information and wish to crack. '
        f'Note: Stegcracker only accepts the following file types: '
        f'{", ".join(Cracker.SUPPORTED_FILES)}'))

    args.add_argument('wordlist', action='store', nargs='?', help=(
        'Wordlist containing the one or more passwords (one password per line). '
        'If no password list is supplied, this will default to the rockyou.txt '
        'wordlist on Kali Linux.'
    ), default=None)

    args.add_argument('-o', '--output', default=None, help=(
        'Output file location, this will be the file the data will be written '
        'to on a successful cracked password. If no output location is '
        'specified, the default location will be the same filename with ".out" '
        'appended to the name.'))

    args.add_argument('-t', '--threads', type=int, default=16, help=(
        'Number of concurrent threads used to crack passwords with, increasing '
        'this number might lead to better performance. Default: 16'))

    args.add_argument('-c', '--chunk-size', type=int, default=64, help=(
        'Number of passwords loaded into memory per thread cycle. After each '
        'password of the chunk has been depleted a status update will be '
        'printed to the console with the attempted password. Default: 64'))

    args.add_argument('-q', '--quiet', '--stfu', action='store_true', help=(
        'Runs the program in "quiet mode", meaning no status updates or other '
        'output besides the cracked password will be echoed to the terminal. '
        'By default, all logging / error messages are printed to stderr (making '
        'piping to other processes easier).'
    ), default=False)

    args.add_argument('-v', '--version', action='store_true', help=(
        'Print the current version number and exit.'))

    args.add_argument('-V', '--verbose', action='store_true', help=(
        'Runs the program in "verbose mode", this will print additional '
        'debugging information (include this output when submitting bug '
        'reports). Cannot be used in conjunction with the "--quiet" argument.'
    ), default=False)

    args = args.parse_args()

    output = args.output or args.file + '.out'
    extension = args.file.split('.')[::-1][0].lower()

    if not find_executable('steghide'):
        return error(
            'Steghide does not appear to be installed, or has not been added to '
            'your current PATH, please install it using: "apt-get install '
            'steghide -y" or by downloading it from the official code '
            'repository: http://steghide.sourceforge.net/')

    if args.wordlist is None:
        if isfile(DEFAULT_WORDLIST_PATH):
            log('No wordlist was specified, using default rockyou.txt wordlist.')
            args.wordlist = DEFAULT_WORDLIST_PATH

        elif isfile(DEFAULT_WORDLIST_PATH + '.gz'):
            return error(
                f'No wordlist was specified, but a gzipped variant of the '
                f'rockyou.txt wordlist was found on your system. If you wish '
                f'to use it, please decompress it first using the following command: '
                f'gzip -d {DEFAULT_WORDLIST_PATH}.gz')

        else:
            return error(
                'No wordlist was specified. '
                'For more help, please run: stegcracker --help')

    if isfile(output):
        return error(f'Output file {output!r} already exists!')

    if not isfile(args.file):
        return error(f'Input file {args.file!r} does not exist!')

    if not isfile(args.wordlist):
        return error(f'Wordlist {args.wordlist!r} does not exist!')

    if args.wordlist.endswith('.gz'):
        return error(
            f"It appears you're using a gzipped variant of a wordlist, instead "
            f"of the actual wordlist itself. You can decompress the gzipped "
            f"using the following command: gzip -d {args.wordlist}")

    if extension not in Cracker.SUPPORTED_FILES:
        return error(
            f'Unsupported file type {extension!r}! Supported '
            f'extensions: {", ".join(Cracker.SUPPORTED_FILES)}')

    if not args.quiet:
        log(f'Counting lines in wordlist..')
        line_count = wc(args.wordlist)
    else:
        line_count = None

    with open(args.wordlist, mode='rb') as wordlist:
        cracker = Cracker(
            file=args.file,
            output=output,
            line_count=line_count,
            chunk_size=args.chunk_size,
            quiet=args.quiet,
            verbose=args.verbose,
            threads=args.threads)

        if not args.quiet:
            log(
                f'Attacking file {args.file!r} '
                f'with wordlist {args.wordlist!r}..')

        cracker.run(wordlist)

    if cracker.password:
        log(f'Successfully cracked file with password: {cracker.password}')
        log(f'Tried {cracker.attempts} passwords')
        log(f'Your file has been written to: {output}')
        print(cracker.password)

    elif cracker.has_error:
        return error('Terminating due to previous exception..')

    else:
        return error('Failed to crack file, ran out of passwords.')


if __name__ == '__main__':
    exit(main() or 0)
