<?php

namespace App\Builders;

use Carbon\Carbon;
use App\Models\Resource;
use Illuminate\Support\Collection;
use App\Services\DataConnectionService;
use App\Generators\GaugeChartScriptGenerator;

class MetricBuilder
{
    private $metric;
    private $connection;
    private $query;
    private $options;
    private $dataConnectionService;

    public function __construct(Resource $metric)
    {
        $this->metric                = $metric;
        $this->dataConnectionService = new DataConnectionService();
        $this->populateOptions();
    }

    private function initializeConnection()
    {
        $this->query = (new DataConnectionService())
            ->getConnection(
                isset($this->metric->data_connection_id)
                ? $this->metric->data_connection_id
                : 'default'
            );

        return $this;

    }

    private function populateOptions()
    {
        $this->options = [
            'performance_data_table' => $this->metric->getResourceConfiguration('performance_data_table'),
            'calculation_column'     => $this->metric->getResourceConfiguration('calculation_column'),
            'function'               => $this->metric->getResourceConfiguration('function'),
            'metric_direction'       => $this->metric->getResourceConfiguration('metric_direction'),
            'filtration_column'      => $this->metric->getResourceConfiguration('filtration_column'),
            'filtration_value'       => $this->metric->getResourceConfiguration('filtration_value'),
            'date_filter_column'     => $this->metric->getResourceConfiguration('date_filter_column'),
            'date_range'             => $this->metric->getResourceConfiguration('date_range'),
            'display_format'         => $this->metric->getResourceConfiguration('display_format'),
            'custom_unit_input'      => $this->metric->getResourceConfiguration('custom_unit_input'),
            'compare_with'           => $this->metric->getResourceConfiguration('compare_with'),
            'target_value_input'     => $this->metric->getResourceConfiguration('target_value_input'),
            'metric_type'            => $this->metric->getResourceConfiguration('metric_type'),
            'metric_color'           => $this->metric->getResourceConfiguration('metric_color')
        ];

        return $this;
    }

    private function addPerformanceTable()
    {
        $this->query = $this->query->table(
            $this->options['performance_data_table']
        );

        return $this;
    }

    private function addAxis()
    {
        $this->query = $this->query->selectRaw(
            $this->getCalculationFunction()
        );
        return $this;
    }

    private function addDateFilters()
    {

        if ($this->options['date_range'] != "all_time") {
            $this->query = $this->query->whereBetween(
                $this->getDateFilterColumn(),
                $this->getDatesBasedOnDateRange()
            );

        }

        return $this;
    }

    private function addFilterColumn()
    {

        if (isset($this->options['filtration_column']) && !empty($this->options['filtration_column'])) {
            $this->query = $this->query->where(
                $this->getFilterColumn(), 'LIKE',
                "%" . $this->getFilterValue() . "%"
            );

        }

        return $this;
    }

    private function addPreviousYearDateFilters()
    {
        $dates = $this->subYearFromDate(
            $this->getDatesBasedOnDateRange()
        );

        if ($this->options['date_range'] != "all_time") {
            $this->query = $this->query->whereBetween(
                $this->getDateFilterColumn(),
                $dates
            );

        }

        return $this;
    }

    private function addPrecedingPeriodDateFilters()
    {

        if ($this->options['date_range'] != "all_time") {
            $this->query = $this->query->whereBetween(
                $this->getDateFilterColumn(),
                $this->getPrecedingPeriodDates()
            );

        }

        return $this;
    }

    private function subYearFromDate($dates)
    {
        return [
            Carbon::parse($dates[0])->subYear()->toDateTimeString(),
            Carbon::parse($dates[1])->subYear()->toDateTimeString()
        ];
    }

    private function getDatesBasedOnDateRange()
    {

        if (!empty($this->options['date_range']) && $this->options['date_range'] != "all_time") {

            switch ($this->options['date_range']) {
                case 'this_3_months':
                    return [now()->subMonths(2)->startOfMonth()->toDateTimeString(), now()->endOfMonth()->toDateTimeString()];

                    break;
                case 'last_3_months':
                    return [now()->subMonths(3)->startOfMonth()->toDateTimeString(), now()->subMonths(1)->endOfMonth()->toDateTimeString()];

                    break;
                case 'this_month':
                    return [now()->firstOfMonth()->startOfDay()->toDateTimeString(), now()->endOfMonth()->endOfDay()->toDateTimeString()];

                    break;
                case 'last_30_days':
                    return [now()->subDays(30)->startOfDay()->toDateTimeString(), now()->subDay()->endOfDay()->toDateTimeString()];

                    break;
                case 'this_week':
                    return [now()->startOfWeek(getStartOfWeek())->toDateTimeString(), now()->endOfWeek(getEndOfWeek())->toDateTimeString()];

                    break;
                case 'last_7_days':
                    return [now()->subDays(7)->startOfDay()->toDateTimeString(), now()->subDay()->endOfDay()->toDateTimeString()];

                    break;
                case 'today':
                    return [now()->startOfDay()->toDateTimeString(), now()->endOfDay()->toDateTimeString()];

                    break;
                case 'yesterday':
                    return [now()->subDay()->startOfDay()->toDateTimeString(), now()->subDay()->endOfDay()->toDateTimeString()];
                    break;
                default:
                    break;
            }

        }

    }

    private function getPrecedingPeriodDates()
    {

        if (!empty($this->options['date_range']) && $this->options['date_range'] != "all_time") {

            $originalTime = Carbon::parse($this->getDatesBasedOnDateRange()[0])->toImmutable();

            switch ($this->options['date_range']) {
                case 'this_3_months':
                    return [$originalTime->subMonths(3)->toDateTimeString(), $originalTime->subMonth()->endOfMonth()->toDateTimeString()];
                    break;

                case 'last_3_months':
                    return [$originalTime->subMonths(3)->startOfMonth()->toDateTimeString(), $originalTime->subMonth()->endOfMonth()->toDateTimeString()];
                    break;

                case 'this_month':
                    return [$originalTime->subMonth()->startOfMonth()->toDateTimeString(), $originalTime->subMonth()->endOfMonth()->toDateTimeString()];
                    break;

                case 'last_30_days':
                    return [$originalTime->subDays(30)->startOfDay()->toDateTimeString(), $originalTime->endOfDay()->toDateTimeString()];
                    break;

                case 'this_week':
                    return [$originalTime->startOfWeek(getStartOfWeek())->subWeek()->startOfDay()->toDateTimeString(), $originalTime->endOfWeek(getEndOfWeek())->subWeek()->endOfDay()->toDateTimeString()];
                    break;

                case 'last_7_days':
                    return [$originalTime->subDays(7)->startOfDay()->toDateTimeString(), $originalTime->subDay()->endOfDay()->toDateTimeString()];
                    break;

                case 'today':
                    return [$originalTime->subDay()->startOfDay()->toDateTimeString(), $originalTime->subDay()->endOfDay()->toDateTimeString()];
                    break;

                case 'yesterday':
                    return [$originalTime->subDays(1)->startOfDay()->toDateTimeString(), now()->subDays(2)->endOfDay()->toDateTimeString()];
                    break;
                default:
                    break;
            }

        }

    }

    private function getCalculationFunction()
    {
        return "{$this->getSelectFunction()}{$this->getCalculationColumn()} as metric";
    }

    private function getSelectFunction()
    {

        switch ($this->options['function']) {
            case 'count':
            case 'conditional_count':
                return 'COUNT(';
                break;
            case 'distinct_count':
                return 'COUNT(DISTINCT ';
                break;
            case 'sum':
            case 'conditional_sum':
                return 'SUM(';
                break;
            case 'average':
            case 'conditional_average':
                return 'AVG(';
                break;
            case 'max':
            case 'conditional_max':
                return 'MAX(';
                break;
            case 'min':
            case 'conditional_min':
                return 'MIN(';
                break;
            case 'sample-standard-deviation':
                return 'STDDEV(';
                break;
            case 'population-standard-deviation':
                return 'STDDEV_POP(';
                break;
            default:
                break;
        }

    }

    private function getSelect()
    {
        return "{$this->getCalculationFunction()}";
    }

    private function getCalculationColumn()
    {

        return "{$this->options['performance_data_table']}.{$this->options['calculation_column']})";
    }

    private function getDateFilterColumn()
    {

        return "{$this->options['performance_data_table']}.{$this->options['date_filter_column']}";
    }

    private function getFilterColumn()
    {

        return "{$this->options['performance_data_table']}.{$this->options['filtration_column']}";
    }

    private function getFilterValue()
    {

        return $this->options['filtration_value'] ?? "";
    }

    private function getAxisValues($query)
    {

        if (!$query instanceof Collection) {
            return intval($query);
        }

        $axisValues = json_decode(json_encode($query), true);

        $originalMetric = $axisValues[0]['metric'];

        return $originalMetric;

    }

    private function buildOriginalMetricWithoutFilterColumn()
    {
        $this->initializeConnection()
            ->populateOptions()
            ->addPerformanceTable()
            ->addAxis()
            ->addDateFilters();
        return $this->query->get();

    }

    private function buildOriginalMetric()
    {
        $this->initializeConnection()
            ->populateOptions()
            ->addPerformanceTable()
            ->addAxis()
            ->addDateFilters();

        if ($this->options['display_format'] == "percentage_of_total") {
            $originalWithoutFilter = clone $this->query->get();
            $this->addFilterColumn();
            $originalWithFilter = clone $this->query->get();

            $metric = is_numeric($originalWithoutFilter->first()->metric) && $originalWithoutFilter->first()->metric != 0
            ? round(($originalWithFilter->first()->metric / $originalWithoutFilter->first()->metric) * 100)
            : 0;

            return new Collection([
                ['metric' => $metric]
            ]);
        }

        $this->addFilterColumn();
        return $this->query->get();
    }

    private function getDisplayUnit()
    {

        switch ($this->options['display_format']) {
            case 'numeric_with_custom_unit':
            case 'compact_currency':
            case 'standard_currency':
                return "{$this->options['custom_unit_input']}";
                break;
            case 'percentage':
            case 'percentage_of_total':
                return "%";
                break;
            default:
                break;
        }

    }

    private function getLastYearComparison()
    {
        $this->initializeConnection()
            ->populateOptions()
            ->addPerformanceTable()
            ->addAxis()
            ->addPreviousYearDateFilters();

        if ($this->options['display_format'] == "percentage_of_total") {
            $originalWithoutFilter = clone $this->query->get();

            $this->addFilterColumn();
            $originalWithFilter = clone $this->query->get();

            $metric = is_numeric($originalWithoutFilter->first()->metric) && $originalWithoutFilter->first()->metric != 0
            ? round(($originalWithFilter->first()->metric / $originalWithoutFilter->first()->metric) * 100)
            : 0;

            return new Collection([
                ['metric' => $metric]
            ]);
        }

        $this->addFilterColumn();
        return $this->query->get();

    }

    private function getPrecedingPeriodComparison()
    {

        $this->initializeConnection()
            ->populateOptions()
            ->addPerformanceTable()
            ->addAxis()
            ->addPrecedingPeriodDateFilters();

        if ($this->options['display_format'] == "percentage_of_total") {
            $originalWithoutFilter = clone $this->query->get();

            $this->addFilterColumn();
            $originalWithFilter = clone $this->query->get();

            $metric = is_numeric($originalWithoutFilter->first()->metric) && $originalWithoutFilter->first()->metric != 0
            ? round(($originalWithFilter->first()->metric / $originalWithoutFilter->first()->metric) * 100)
            : 0;

            return new Collection([
                ['metric' => $metric]
            ]);
        }

        $this->addFilterColumn();
        return $this->query->get();

    }

    private function buildComparisonMetric()
    {

        switch ($this->options['compare_with']) {
            case 'compare_with_same_time_last_year':
                return $this->getLastYearComparison();
                break;
            case 'compare_with_preceding_period':
                return $this->getPrecedingPeriodComparison();
                break;
            case 'compare_with_a_fixed_target_value':
                return $this->options['target_value_input'];
                break;
            default:
                return;
                break;
        }

    }

    private function calculateChange($original, $comparison)
    {

        if ($this->options['metric_direction'] == "increase") {
            return $original - $comparison;
        } else {
            return $comparison - $original;
        }

    }

    private function calculateTrend($original, $comparison)
    {

        if ($comparison <= 0) {
            return '∞';
        }

        return round(abs(($this->calculateChange($original, $comparison) / $comparison) * 100), 1);
    }

    private function displayFormat($number)
    {

        switch ($this->options['display_format']) {
            case 'numeric':
            case 'percentage':
            case 'percentage_of_total':
                return $number . $this->getDisplayUnit();
            case 'numeric_with_custom_unit':
                return $number . " " . $this->getDisplayUnit();
            case 'compact_currency':
                return formatCurrencyCompact($number, $this->options['custom_unit_input']);
            case 'standard_currency':
                return formatStandardCurrency($number, $this->options['custom_unit_input']);
            default:
                break;
        }

    }

    private function tooltip($original, $trend, $trendDirection, $change)
    {
        $tooltip = "<span class='tooltip-bold'>Performance comparison:</span><br>";
        $tooltip .= !in_array($this->options['display_format'], ["percentage_of_total", "percentage"])
        ? $change : "";

        $tooltip = $trendDirection == "positive" ?
        $tooltip . " <span class='tooltip-green'>(" . $trend . "%)</span>  more than "
        : $tooltip . " <span class='tooltip-red'>(" . $trend . "%)</span>  less than ";

        switch ($this->options['compare_with']) {
            case 'compare_with_same_time_last_year':
                return $tooltip . "the ​same time last year";
                break;
            case 'compare_with_preceding_period':
                return $tooltip . "the preceding period";
                break;
            case 'compare_with_a_fixed_target_value':
                return $tooltip . "the target value of " . $this->displayFormat($this->options['target_value_input']);
                break;
            default:
                return;
                break;
        }

        return $tooltip;
    }

    private function drillTooltip($original, $trend, $trendDirection, $change)
    {
        $this->metric->getResourceConfiguration('title');
        $tooltip = "<span class='tooltip-bold'>{$this->metric->getResourceConfiguration('title')} : {$this->displayFormat($original)}</span><br>";
        $tooltip .= !in_array($this->options['display_format'], ["percentage_of_total", "percentage"])
        ? $change : "";

        $tooltip = $trendDirection == "positive" ?
        $tooltip . " <span class='tooltip-green'>(" . $trend . "%)</span>  more than "
        : $tooltip . " <span class='tooltip-red'>(" . $trend . "%)</span>  less than ";

        switch ($this->options['compare_with']) {
            case 'compare_with_same_time_last_year':
                return $tooltip . "the ​same time last year";
                break;
            case 'compare_with_preceding_period':
                return $tooltip . "the preceding period";
                break;
            case 'compare_with_a_fixed_target_value':
                return $tooltip . "the target value of " . $this->displayFormat($this->options['target_value_input']);
                break;
            default:
                return;
                break;
        }

        return $tooltip;
    }

    public function buildGauge($originalMetric, $comparisonMetric, $change, $trend, $trendDirection, $size = "small")
    {
        $this->metric->script = (new GaugeChartScriptGenerator(
            $this->metric,
            [$originalMetric, $comparisonMetric, $change, $trend, $trendDirection]
        ))
            ->getGaugeScript($size);

        $this->metric->tooltip = $this->tooltip($originalMetric, $trend, $trendDirection, $change);

        $this->metric->drillTooltip = $this->drillTooltip($originalMetric, $trend, $trendDirection, $change);

        return $this->metric->script;
    }

    public function build($gauge = false, $size = "small")
    {
        $this->initializeConnection();

        if(!$this->query && !is_null($this->query)){
            return $this->metric->data = [
                "error"  => "There is a problem with the chosen data connection.",
            ];

        }

        $originalMetric   = round($this->getAxisValues($this->buildOriginalMetric()), 1);
        $comparisonMetric = round($this->getAxisValues($this->buildComparisonMetric()), 1);

        $calcChange = $this->calculateChange($originalMetric, $comparisonMetric);

        $change         = $this->displayFormat($calcChange > 0 ? "+" . round($calcChange, 1) : round($calcChange, 1));
        $trend          = $this->calculateTrend($originalMetric, $comparisonMetric);
        $trendDirection = $this->calculateChange($originalMetric, $comparisonMetric) > 0 ? "positive" : "negative";

        $tooltip = $this->tooltip($change, $trend, $trendDirection, $change);

        if (!$gauge) {
            $this->metric->data = [
                "original_metric"  => $this->displayFormat($originalMetric),
                "comparison_metic" => $comparisonMetric,
                "metric_direction" => $this->options['metric_direction'],
                "trend_direction"  => $trendDirection,
                "change"           => $change,
                "trend"            => $trend,
                "tooltip"          => $tooltip
            ];
        }

// dd($originalMetric, $comparisonMetric, $change, $trend, $trendDirection, $size);

        if ($this->metric->configurations->chart_type == "gauge_chart" || $gauge) {
            return $this->buildGauge($originalMetric, $comparisonMetric, $change, $trend, $trendDirection, $size);
        }

    }

}
