"""
This script verifies models against a standard set of images.
"""

# pylint: disable=redefined-outer-name

import argparse
import logging
import math
import os

from pumaguard.presets import (
    Preset,
)
from pumaguard.utils import (
    classify_image,
)

logger = logging.getLogger('PumaGuard')


def configure_subparser(parser: argparse.ArgumentParser):
    """
    Parse the commandline
    """
    parser.add_argument(
        '--verification-path',
        help='Path to verification data set (default = %(default)s)',
    )
    parser.add_argument(
        'image',
        metavar='FILE',
        help='An image to classify.',
        nargs='*',
        type=str,
    )


def get_accuracy(predictions: list[tuple[str, float, int]]) -> float:
    """
    Get the accuracy of the model.
    """
    confusion_matrix = {
        'TP': 0.0, 'TN': 0.0, 'FP': 0.0, 'FN': 0.0,
    }
    number_lion = 0
    number_no_lion = 0
    for _, prediction, label in predictions:
        if label == 0:
            if prediction >= 0:
                confusion_matrix['TP'] += 1 - prediction
                confusion_matrix['FN'] += prediction
                number_lion += 1
            else:
                logger.warning('predicted label < 0!')
        elif label == 1:
            if prediction >= 0:
                confusion_matrix['TN'] += prediction
                confusion_matrix['FP'] += 1 - prediction
                number_no_lion += 1
            else:
                logger.warning('predicted label < 0!')
    total = sum(confusion_matrix.values())
    logger.debug('total = %.2f', total)
    logger.debug('%d lion and %d no-lion images', number_lion, number_no_lion)
    logger.debug('confusion matrix: %s', confusion_matrix)
    return (confusion_matrix['TP'] + confusion_matrix['TN']) / len(predictions)


def get_binary_accuracy(predictions: list[tuple[str, float, int]]) -> float:
    """
    Get the accuracy of the model.
    """
    confusion_matrix = [[0, 0], [0, 0]]
    for _, prediction, label in predictions:
        prediction = round(prediction)
        confusion_matrix[label][prediction] += 1
    logger.debug('binary confusion matrix: %s', confusion_matrix)
    return (confusion_matrix[0][0] + confusion_matrix[1][1]) / len(predictions)


def get_crossentropy_loss(predictions: list[tuple[str, float, int]]) -> float:
    """
    Get the log-loss (crossentropy loss) of the model.
    """
    loss: float = 0
    epsilon = 1e-15
    for _, prediction, label in predictions:
        prediction = max(epsilon, min(1 - epsilon, prediction))
        loss += label * math.log(prediction) + \
            (1 - label) * math.log(1 - prediction)
    return -loss / len(predictions)


def get_mean_squared_error(predictions: list[tuple[str, float, int]]) -> float:
    """
    Get the mean squared error of the model.
    """
    error: float = 0
    for _, prediction, label in predictions:
        error += (label - prediction)**2
    return error / len(predictions)


def verify_model(presets: Preset):
    """
    Verify a model by calculating its accuracy across a standard set of images.
    """
    logger.info('verifying model')
    lion_directory = os.path.join(presets.verification_path, 'lion')
    lions = sorted(os.listdir(lion_directory))
    no_lion_directory = os.path.join(presets.verification_path, 'no-lion')
    no_lions = sorted(os.listdir(no_lion_directory))
    logger.debug('%d lions and %d no lions', len(lions), len(no_lions))
    predictions = []
    number_false_positives = 0
    number_false_negatives = 0
    for lion in lions:
        filename = os.path.relpath(os.path.join(lion_directory, lion), '.')
        logger.debug('classifying %s', filename)
        prediction = classify_image(presets, filename)
        predictions.append((filename, prediction, 0))
        is_correct = prediction < 0.5
        if not is_correct:
            number_false_negatives += 1
        logger.info('Predicted %s: label %.4f, %6.2f%% lion: %s',
                    filename, prediction, 100*(1 - prediction),
                    'correct' if is_correct else 'incorrect')
    for no_lion in no_lions:
        filename = os.path.relpath(
            os.path.join(no_lion_directory, no_lion), '.')
        logger.debug('classifying %s', filename)
        prediction = classify_image(presets, filename)
        predictions.append((filename, prediction, 1))
        is_correct = prediction >= 0.5
        if not is_correct:
            number_false_positives += 1
        logger.info('Predicted %s: label %.4f, %6.2f%% lion: %s',
                    filename, prediction, 100*(1 - prediction),
                    'correct' if is_correct else 'incorrect')
    print(f'number false positives = {number_false_positives}')
    print(f'number false negatives = {number_false_negatives}')
    print(f'out of {len(lions)} lion and {len(no_lions)} no-lion images')
    print('accuracy           = '
          f'{100 * get_accuracy(predictions):.2f}%')
    print('binary accuracy    = '
          f'{100 * get_binary_accuracy(predictions):.2f}%')
    print('crossentropy loss  = '
          f'{get_crossentropy_loss(predictions):.4f}')
    print('mean squared error = '
          f'{get_mean_squared_error(predictions):.4f}')


def main(presets: Preset):
    """
    Main entry point
    """

    verify_model(presets)
