#!/usr/bin/python3
from enum import Enum
from pathlib import Path
from sys import exit
import argparse
from datetime import datetime, timedelta, timezone
from typing import NoReturn, Optional

from pydantic import BaseModel

from stackit_cost_monitoring.cost_api import CostApi, CostApiGranularity, CostApiException, CostApiDepth, CostApiItem
from stackit_cost_monitoring.auth import Auth, AuthException

SECONDS_PER_DAY = 24 * 3600
CENTS_PER_EURO = 100

DEFAULT_WARNING_EUROS = 10.0
DEFAULT_CRITICAL_EUROS = 50.0
DEFAULT_SA_KEY_JSON = Path.home() / ".stackit" / "sa-key.json"


class NagiosExitCodes(Enum):
    OK = 0
    WARNING = 1
    CRITICAL = 2
    UNKNOWN = 3


class ParsedArguments(BaseModel):
    customer_account_id: str
    project_id: str
    warning: float
    critical: float
    sa_key_json: Path
    skip_discounts: bool
    api_log_file: Optional[Path]


class NagiosReporter:
    def __init__(self, args: ParsedArguments):
        self.args = args
        self._report_date = None
        self._cost = None
        self._discounted_cost = None
        self._report_data_message = None

    def book_cost_item(self, cost_item: CostApiItem):
        if cost_item.reportData is None:
            self._book_total_cost(cost_item, 'CostApi returned no reportData')
            return
        if len(cost_item.reportData) == 0:
            self.__book_total_cost(cost_item, 'CostApi returned empty reportData')
            return
        for report_data in cost_item.reportData:
            if self._report_date is not None and report_data.timePeriod.start < self._report_date:
                continue
            self._report_date = report_data.timePeriod.start
            self._cost = report_data.charge / CENTS_PER_EURO
            self._discounted_cost = report_data.discount / CENTS_PER_EURO

    def _book_total_cost(self, cost_item: CostApiItem, message: str):
        self._report_data_message = message
        self._cost = cost_item.totalCharge / CENTS_PER_EURO
        self._discounted_cost = cost_item.totalDiscount / CENTS_PER_EURO

    def do_report(self) -> NoReturn:
        cost = self._cost
        """"
            StackIt's answer to ticket SSD-13595:
            
                The totalCharge value in the API response already includes all granted discounts.
                
            To detect pathological effects we should add the discounted costs to get an
            alarm before all our free budget has been used. By default we add the discounts.
        """
        if self._cost is None:
            if self._report_data_message is None:
                return self._finish(
                    NagiosExitCodes.UNKNOWN,
                    'Internal error: Have no data and do not know why'
                )
            else:
                return self._finish(
                    NagiosExitCodes.OK,
                    f"Zero costs ({self._report_data_message})"
                )

        if not self.args.skip_discounts:
            cost += self._discounted_cost
        if self._report_date is None:
            report_date_str = '(unknown date - no detailed report data)'
        else:
            data_str = self._report_date.strftime('%Y-%m-%d')
            report_date_str = f"for {data_str}"

        if cost >= self.args.critical:
            exit_code = NagiosExitCodes.CRITICAL
            message = f"Daily costs {cost:.2f} EUR >= {self.args.critical} EUR {report_date_str}"
        elif cost >= self.args.warning:
            exit_code = NagiosExitCodes.WARNING
            message = f"Daily costs {cost:.2f} EUR >= {self.args.warning} EUR {report_date_str}"
        elif cost > 0 and self._report_data_message is not None:
            exit_code = NagiosExitCodes.WARNING
            message = f"No detailed reportData ({self._report_data_message}) " \
                    f"but non-zero costs {cost:.2f} EUR"
        else:
            exit_code = NagiosExitCodes.OK
            message = f"Daily costs {cost:.2f} EUR {report_date_str}"

        return self._finish(exit_code, message)

    def _finish(self, status: NagiosExitCodes, message: str) -> NoReturn:
        warning = self.args.warning
        critical = self.args.critical
        if self._cost is not None:
            perf_data_items = [
                f"cost={self._cost:.2f};{warning:.2f};{critical:.2f};0",
                f"discounted_cost={self._discounted_cost:.2f};{warning:.2f};{critical:.2f};0"
            ]
        else:
            perf_data_items = []
        perf_data = ' '.join(perf_data_items)
        print(f"{status.name}: {message} | {perf_data}")
        return exit(status.value)


def main():
    try:
        args = get_arguments()
        cost_item = get_cost(args)
        reporter = NagiosReporter(args)
        reporter.book_cost_item(cost_item)
        reporter.do_report()
    except (AuthException, CostApiException) as e:
        print(f"{NagiosExitCodes.UNKNOWN.name}: {e} |")
        exit(NagiosExitCodes.UNKNOWN.value)


def get_arguments() -> ParsedArguments:
    parser = argparse.ArgumentParser(
        description='Nagios plugin to monitor StackIT costs. The higher value '
        'of the cost of the present day (always 0?) and yesterday is used.',
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    parser.add_argument(
        '--customer-account-id',
        required=True,
        help='StackIT customer account ID'
    )
    parser.add_argument(
        '--project-id',
        required=True,
        help='StackIT project ID'
    )
    parser.add_argument(
        '-w', '--warning',
        type=float,
        default=DEFAULT_WARNING_EUROS,
        help=f"Warning threshold for 24h cost in EUR (default: {DEFAULT_WARNING_EUROS:.2f})"
    )
    parser.add_argument(
        '-c', '--critical',
        type=float,
        default=DEFAULT_CRITICAL_EUROS,
        help=f"Critical threshold for 24h cost in EUR (default: {DEFAULT_CRITICAL_EUROS:.2f})"
    )
    parser.add_argument(
        '--sa-key-json',
        type=Path,
        default=DEFAULT_SA_KEY_JSON,
        help=f"Path to StackIT credentials in JSON format (default: {DEFAULT_SA_KEY_JSON})"
    )
    parser.add_argument(
        '--skip-discounts',
        action='store_true',
        help='Skip discounted costs in calculation.'
    )
    parser.add_argument(
        '--api-log-file',
        type=Path,
        required=False,
        help='Optional path to file where the API requests and responses will be logged.'
    )

    parsed_arguments = ParsedArguments(**parser.parse_args().__dict__)
    if parsed_arguments.warning < 0.0:
        raise ValueError("Warning threshold must be >= 0.0")
    if parsed_arguments.critical <= parsed_arguments.warning:
        raise ValueError("Critical threshold must be > warning threshold")
    return parsed_arguments


def get_cost(args) -> CostApiItem:
    auth = Auth(args.sa_key_json)
    api_log = None
    try:
        if args.api_log_file is not None:
            api_log = open(args.api_log_file, 'a')
        cost_api = CostApi(auth, api_log=api_log)
        today = datetime.now(timezone.utc).date()  # StackIT, ticket SSD-13595: UTC is used
        yesterday = today - timedelta(days=1)
        two_days_ago = today - timedelta(days=2)

        result = cost_api.get_project_costs(
            args.customer_account_id,
            args.project_id,
            from_date=two_days_ago,
            to_date=yesterday,
            granularity=CostApiGranularity.DAILY,
            depth=CostApiDepth.PROJECT,
            include_zero_costs=True,
        )

        return result
    finally:
        if api_log is not None:
            api_log.close()


if __name__ == '__main__':
    main()
