<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Invoice;
use App\Models\Project;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use Morilog\Jalali\Jalalian;

class ProjectProfitChartService
{
    private const MAX_MONTHS = 24;

    private const SUCCESSFUL_INVOICE_STATUSES = [
        Invoice::STATUS_PAID,
        Invoice::STATUS_APPROVED,
    ];

    /**
     * @return list<array{label: string, value: int, date: string}>
     */
    public function chartPointsForProject(Project $project): array
    {
        $months = $this->monthRangeForProject($project);
        $invoices = $this->relevantInvoices($project);

        if ($invoices->isNotEmpty()) {
            return $this->cumulativeFromMonthlyNets(
                $this->monthlyNetsFromInvoices($invoices),
                $months
            );
        }

        return $this->cumulativeFromEstimate((int) ($project->estimated_profit ?? 0), $months);
    }

    /**
     * Attach chart_points onto each project model for resource serialization.
     *
     * @param  Collection<int, Project>|EloquentCollection<int, Project>|iterable<Project>  $projects
     */
    public function attachChartPoints(iterable $projects): void
    {
        foreach ($projects as $project) {
            $project->setAttribute('chart_points', $this->chartPointsForProject($project));
        }
    }

    /**
     * Portfolio-level series for overview / overview profit-chart.
     *
     * @param  Collection<int, Project>|EloquentCollection<int, Project>  $projects
     * @return array{
     *     total_profit: int,
     *     profit_percent: float,
     *     profit_series: list<array{label: string, value: int, date: string}>,
     *     loss_series: list<array{label: string, value: int, date: string}>,
     *     tooltip: array{value: int, date: string}|null
     * }
     */
    public function portfolioSeries(Collection|EloquentCollection $projects): array
    {
        $totalInvestment = (int) $projects->sum(fn (Project $p) => (int) ($p->budget ?? 0));
        $totalProfit = (int) $projects->sum(fn (Project $p) => (int) ($p->estimated_profit ?? 0));

        $months = $this->monthRangeForPortfolio($projects);

        if ($months === []) {
            return [
                'total_profit' => $totalProfit,
                'profit_percent' => $this->percent($totalProfit, $totalInvestment),
                'profit_series' => [],
                'loss_series' => [],
                'tooltip' => null,
            ];
        }

        $monthlyIncome = [];
        $monthlyExpense = [];
        $estimateOnly = collect();

        foreach ($projects as $project) {
            $invoices = $this->relevantInvoices($project);
            if ($invoices->isEmpty()) {
                $estimateOnly->push($project);

                continue;
            }

            foreach ($invoices as $invoice) {
                $date = $invoice->issued_at ?? $invoice->created_at;
                if (! $date instanceof CarbonInterface) {
                    continue;
                }
                $key = $date->copy()->startOfMonth()->format('Y-m-d');
                $amount = (int) $invoice->total;
                if ($invoice->type === Invoice::TYPE_INCOME) {
                    $monthlyIncome[$key] = ($monthlyIncome[$key] ?? 0) + $amount;
                } else {
                    $monthlyExpense[$key] = ($monthlyExpense[$key] ?? 0) + $amount;
                }
            }
        }

        $profitFromInvoices = $this->cumulativeFromMonthlyAmounts($monthlyIncome, $months);
        $lossSeries = $this->cumulativeFromMonthlyAmounts($monthlyExpense, $months);
        $profitFromEstimates = $estimateOnly->isNotEmpty()
            ? $this->mergeProjectEstimateSeries($estimateOnly, $months)
            : array_map(fn (Carbon $month) => $this->point($month, 0), $months);

        $profitSeries = [];
        foreach ($months as $index => $month) {
            $profitSeries[] = $this->point(
                $month,
                (int) ($profitFromInvoices[$index]['value'] ?? 0) + (int) ($profitFromEstimates[$index]['value'] ?? 0)
            );
        }

        $lastProfit = $profitSeries !== [] ? end($profitSeries) : null;

        return [
            'total_profit' => $totalProfit,
            'profit_percent' => $this->percent($totalProfit, $totalInvestment),
            'profit_series' => $profitSeries,
            'loss_series' => $lossSeries,
            'tooltip' => $lastProfit
                ? ['value' => (int) $lastProfit['value'], 'date' => (string) $lastProfit['date']]
                : null,
        ];
    }

    /**
     * @return Collection<int, Invoice>
     */
    private function relevantInvoices(Project $project): Collection
    {
        if ($project->relationLoaded('invoices')) {
            return $project->invoices
                ->filter(fn (Invoice $invoice) => in_array($invoice->status, self::SUCCESSFUL_INVOICE_STATUSES, true))
                ->values();
        }

        return $project->invoices()
            ->whereIn('status', self::SUCCESSFUL_INVOICE_STATUSES)
            ->get();
    }

    /**
     * @return list<Carbon>
     */
    private function monthRangeForProject(Project $project): array
    {
        $start = ($project->start_date ?? $project->created_at ?? now())->copy()->startOfMonth();
        $endSource = $project->end_date && $project->end_date->lt(now())
            ? $project->end_date
            : now();
        $end = $endSource->copy()->startOfMonth();

        if ($end->lt($start)) {
            $end = $start->copy();
        }

        return $this->buildMonthRange($start, $end);
    }

    /**
     * @param  Collection<int, Project>|EloquentCollection<int, Project>  $projects
     * @return list<Carbon>
     */
    private function monthRangeForPortfolio(Collection|EloquentCollection $projects): array
    {
        if ($projects->isEmpty()) {
            return [];
        }

        $starts = $projects->map(function (Project $project) {
            return ($project->start_date ?? $project->created_at ?? now())->copy()->startOfMonth();
        });

        $start = $starts->min();
        $end = now()->copy()->startOfMonth();

        return $this->buildMonthRange($start, $end);
    }

    /**
     * @return list<Carbon>
     */
    private function buildMonthRange(CarbonInterface $start, CarbonInterface $end): array
    {
        $cursor = $start->copy()->startOfMonth();
        $limit = $end->copy()->startOfMonth();
        $months = [];

        while ($cursor->lte($limit) && count($months) < self::MAX_MONTHS) {
            $months[] = $cursor->copy();
            $cursor->addMonth();
        }

        if ($months === []) {
            $months[] = now()->copy()->startOfMonth();
        }

        return $months;
    }

    /**
     * @param  Collection<int, Invoice>  $invoices
     * @return array<string, int>
     */
    private function monthlyNetsFromInvoices(Collection $invoices): array
    {
        $nets = [];

        foreach ($invoices as $invoice) {
            $date = $invoice->issued_at ?? $invoice->created_at;
            if (! $date instanceof CarbonInterface) {
                continue;
            }
            $key = $date->copy()->startOfMonth()->format('Y-m-d');
            $amount = (int) $invoice->total;
            $delta = $invoice->type === Invoice::TYPE_INCOME ? $amount : -$amount;
            $nets[$key] = ($nets[$key] ?? 0) + $delta;
        }

        return $nets;
    }

    /**
     * @param  array<string, int>  $monthlyNets
     * @param  list<Carbon>  $months
     * @return list<array{label: string, value: int, date: string}>
     */
    private function cumulativeFromMonthlyNets(array $monthlyNets, array $months): array
    {
        $cumulative = 0;
        $points = [];

        foreach ($months as $month) {
            $key = $month->format('Y-m-d');
            $cumulative += $monthlyNets[$key] ?? 0;
            $points[] = $this->point($month, $cumulative);
        }

        return $points;
    }

    /**
     * @param  array<string, int>  $monthlyAmounts
     * @param  list<Carbon>  $months
     * @return list<array{label: string, value: int, date: string}>
     */
    private function cumulativeFromMonthlyAmounts(array $monthlyAmounts, array $months): array
    {
        $cumulative = 0;
        $points = [];

        foreach ($months as $month) {
            $key = $month->format('Y-m-d');
            $cumulative += $monthlyAmounts[$key] ?? 0;
            $points[] = $this->point($month, $cumulative);
        }

        return $points;
    }

    /**
     * @param  list<Carbon>  $months
     * @return list<array{label: string, value: int, date: string}>
     */
    private function cumulativeFromEstimate(int $targetProfit, array $months): array
    {
        $count = count($months);
        if ($count === 0) {
            return [$this->point(now()->startOfMonth(), $targetProfit)];
        }

        $points = [];
        foreach ($months as $index => $month) {
            $progress = ($index + 1) / $count;
            $points[] = $this->point($month, (int) round($targetProfit * $progress));
        }

        return $points;
    }

    /**
     * Sum per-project estimated cumulative series onto shared month buckets.
     *
     * @param  Collection<int, Project>|EloquentCollection<int, Project>  $projects
     * @param  list<Carbon>  $months
     * @return list<array{label: string, value: int, date: string}>
     */
    private function mergeProjectEstimateSeries(Collection|EloquentCollection $projects, array $months): array
    {
        $totals = array_fill(0, count($months), 0);

        foreach ($projects as $project) {
            $projectMonths = $this->monthRangeForProject($project);
            $series = $this->cumulativeFromEstimate((int) ($project->estimated_profit ?? 0), $projectMonths);
            $byDate = [];
            foreach ($series as $point) {
                $byDate[$point['date']] = $point['value'];
            }

            $last = 0;
            foreach ($months as $index => $month) {
                $key = $month->toDateString();
                if (isset($byDate[$key])) {
                    $last = $byDate[$key];
                } elseif ($month->lt($projectMonths[0] ?? $month)) {
                    $last = 0;
                }
                $totals[$index] += $last;
            }
        }

        $points = [];
        foreach ($months as $index => $month) {
            $points[] = $this->point($month, $totals[$index]);
        }

        return $points;
    }

    /**
     * @return array{label: string, value: int, date: string}
     */
    private function point(CarbonInterface $month, int $value): array
    {
        $jalali = Jalalian::fromCarbon(Carbon::instance($month->copy()->startOfMonth()));

        return [
            'label' => $jalali->format('m'),
            'value' => $value,
            'date' => $month->copy()->startOfMonth()->toDateString(),
        ];
    }

    private function percent(int $profit, int $investment): float
    {
        if ($investment <= 0) {
            return 0.0;
        }

        return round(($profit / $investment) * 100, 1);
    }
}
