<?php

namespace App\Http\Controllers\Reports;

use App\Http\Controllers\Controller;
use App\Models\Account;
use App\Models\CustomerInvoice;
use App\Models\JournalLine;
use App\Models\TransportJob;
use App\Models\VendorBill;
use App\Support\Decimal;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;

final class ReportController extends Controller
{
    public function index(Request $request): View
    {
        $this->permission($request);

        return view('reports.index');
    }

    public function trialBalance(Request $request): View
    {
        $this->permission($request);
        [$from, $to] = $this->dates($request);

        $rows = $this->accountBalances($request, $from, $to)->map(function ($account) {
            $net = Decimal::subtract((string) $account->debit, (string) $account->credit);
            $account->closing_debit = Decimal::compare($net, '0') > 0 ? $net : Decimal::scale('0');
            $account->closing_credit = Decimal::compare($net, '0') < 0 ? Decimal::subtract('0', $net) : Decimal::scale('0');

            return $account;
        });

        $totals = [
            'debit' => $this->sumField($rows, 'closing_debit'),
            'credit' => $this->sumField($rows, 'closing_credit'),
        ];

        return view('reports.trial-balance', compact('rows', 'totals', 'from', 'to'));
    }

    public function profitLoss(Request $request): View
    {
        $this->permission($request);
        [$from, $to] = $this->dates($request);

        $rows = $this->accountBalances($request, $from, $to)
            ->filter(fn ($account) => in_array($account->type, ['income', 'cost_of_revenue', 'expense'], true))
            ->map(function ($account) {
                $account->amount = $account->type === 'income'
                    ? Decimal::subtract((string) $account->credit, (string) $account->debit)
                    : Decimal::subtract((string) $account->debit, (string) $account->credit);

                return $account;
            })
            ->groupBy('type');

        $income = $this->sumField($rows->get('income', collect()), 'amount');
        $cost = $this->sumField($rows->get('cost_of_revenue', collect()), 'amount');
        $expenses = $this->sumField($rows->get('expense', collect()), 'amount');
        $gross = Decimal::subtract($income, $cost);
        $net = Decimal::subtract($gross, $expenses);

        return view('reports.profit-loss', compact('rows', 'income', 'cost', 'expenses', 'gross', 'net', 'from', 'to'));
    }

    public function balanceSheet(Request $request): View
    {
        $this->permission($request);
        $to = $request->date('to')?->toDateString() ?? today()->toDateString();
        $from = '1900-01-01';

        $rows = $this->accountBalances($request, $from, $to)
            ->filter(fn ($account) => in_array($account->type, ['asset', 'liability', 'equity'], true))
            ->map(function ($account) {
                $account->amount = $account->type === 'asset'
                    ? Decimal::subtract((string) $account->debit, (string) $account->credit)
                    : Decimal::subtract((string) $account->credit, (string) $account->debit);

                return $account;
            })
            ->groupBy('type');

        $assets = $this->sumField($rows->get('asset', collect()), 'amount');
        $liabilities = $this->sumField($rows->get('liability', collect()), 'amount');
        $equity = $this->sumField($rows->get('equity', collect()), 'amount');

        return view('reports.balance-sheet', compact('rows', 'assets', 'liabilities', 'equity', 'to'));
    }

    public function generalLedger(Request $request): View
    {
        $this->permission($request);
        [$from, $to] = $this->dates($request);
        $accounts = Account::where('company_id', $request->user()->company_id)->orderBy('code')->get();

        $query = JournalLine::with(['journalEntry', 'account', 'party'])
            ->whereHas('journalEntry', fn ($journal) => $journal
                ->where('company_id', $request->user()->company_id)
                ->whereBetween('posting_date', [$from, $to])
                ->where('status', 'posted'));

        $this->scopeLines($request, $query);

        if ($accountId = $request->integer('account_id')) {
            $query->where('account_id', $accountId);
        }
        if ($partyId = $request->integer('party_id')) {
            $query->where('party_id', $partyId);
        }

        $lines = $query
            ->join('journal_entries', 'journal_lines.journal_entry_id', '=', 'journal_entries.id')
            ->select('journal_lines.*')
            ->orderBy('journal_entries.posting_date')
            ->orderBy('journal_entries.id')
            ->paginate(50)
            ->withQueryString();

        return view('reports.general-ledger', compact('lines', 'accounts', 'from', 'to'));
    }

    public function aging(Request $request, string $side): View
    {
        $this->permission($request);
        abort_unless(in_array($side, ['customer', 'vendor'], true), 404);
        $today = today();

        if ($side === 'customer') {
            $documents = CustomerInvoice::with('party')
                ->where('company_id', $request->user()->company_id)
                ->whereIn('status', ['posted', 'partially_paid', 'overdue'])
                ->get();
        } else {
            $documents = VendorBill::with('party')
                ->where('company_id', $request->user()->company_id)
                ->whereIn('status', ['posted', 'partially_paid', 'overdue'])
                ->get();
        }

        $documents = $documents->map(function ($document) use ($today) {
            $settled = Decimal::add((string) $document->paid_total, (string) $document->allocated_total);
            $document->balance = Decimal::subtract((string) $document->grand_total, $settled);
            $document->days = $document->due_date ? max(0, $document->due_date->diffInDays($today, false)) : 0;
            $document->bucket = $document->days <= 30 ? 'current' : ($document->days <= 60 ? '31_60' : ($document->days <= 90 ? '61_90' : 'over_90'));

            return $document;
        })->filter(fn ($document) => Decimal::compare((string) $document->balance, '0') > 0);

        $groups = $documents->groupBy('party_id')->map(function (Collection $items) {
            $first = $items->first();

            return (object) [
                'party' => $first->party,
                'current' => $this->sumField($items->where('bucket', 'current'), 'balance'),
                '31_60' => $this->sumField($items->where('bucket', '31_60'), 'balance'),
                '61_90' => $this->sumField($items->where('bucket', '61_90'), 'balance'),
                'over_90' => $this->sumField($items->where('bucket', 'over_90'), 'balance'),
                'total' => $this->sumField($items, 'balance'),
            ];
        })->values();

        return view('reports.aging', compact('groups', 'side'));
    }

    public function vat(Request $request): View
    {
        $this->permission($request);
        [$from, $to] = $this->dates($request);

        $sales = DB::table('customer_invoice_items as li')
            ->join('customer_invoices as h', 'li.customer_invoice_id', '=', 'h.id')
            ->leftJoin('tax_codes as t', 'li.tax_code_id', '=', 't.id')
            ->where('h.company_id', $request->user()->company_id)
            ->whereIn('h.status', ['posted', 'partially_paid', 'paid', 'overdue'])
            ->whereBetween('h.issue_date', [$from, $to])
            ->selectRaw("COALESCE(t.code,'NO-TAX') code, COALESCE(t.category,'none') category, SUM(li.taxable_amount) taxable, SUM(li.vat_amount) vat")
            ->groupBy('t.code', 't.category')
            ->get();

        $purchases = DB::table('vendor_bill_items as li')
            ->join('vendor_bills as h', 'li.vendor_bill_id', '=', 'h.id')
            ->leftJoin('tax_codes as t', 'li.tax_code_id', '=', 't.id')
            ->where('h.company_id', $request->user()->company_id)
            ->whereIn('h.status', ['posted', 'partially_paid', 'paid', 'overdue'])
            ->whereBetween('h.invoice_date', [$from, $to])
            ->selectRaw("COALESCE(t.code,'NO-TAX') code, COALESCE(t.category,'none') category, SUM(li.taxable_amount) taxable, SUM(li.recoverable_vat_amount) recoverable_vat, SUM(li.nonrecoverable_vat_amount) nonrecoverable_vat")
            ->groupBy('t.code', 't.category')
            ->get();

        $output = $this->sumField($sales, 'vat');
        $input = $this->sumField($purchases, 'recoverable_vat');

        return view('reports.vat', compact('sales', 'purchases', 'output', 'input', 'from', 'to'));
    }

    public function jobProfitability(Request $request): View
    {
        $this->permission($request);
        [$from, $to] = $this->dates($request);
        $query = TransportJob::with(['customer', 'route'])
            ->where('company_id', $request->user()->company_id)
            ->whereBetween('job_date', [$from, $to]);

        if ($request->user()->branch_id && ! $request->user()->hasPermission('branches.view_all')) {
            $query->where('branch_id', $request->user()->branch_id);
        }

        $jobs = $query->orderByDesc('job_date')->paginate(50)->withQueryString();

        return view('reports.job-profitability', compact('jobs', 'from', 'to'));
    }

    public function export(Request $request, string $report): StreamedResponse
    {
        $this->permission($request);
        [$from, $to] = $this->dates($request);
        $filename = $report.'-'.$from.'-'.$to.'.csv';

        return response()->streamDownload(function () use ($request, $report, $from, $to): void {
            $output = fopen('php://output', 'w');

            if ($report === 'trial-balance') {
                fputcsv($output, ['Code', 'Account', 'Type', 'Debit Movements', 'Credit Movements']);
                foreach ($this->accountBalances($request, $from, $to) as $account) {
                    fputcsv($output, [$account->code, $account->name, $account->type, $account->debit, $account->credit]);
                }
            } elseif ($report === 'jobs') {
                fputcsv($output, ['Job', 'Date', 'Customer', 'Revenue', 'Cost', 'Profit', 'Status']);
                foreach (TransportJob::with('customer')->where('company_id', $request->user()->company_id)->whereBetween('job_date', [$from, $to])->cursor() as $job) {
                    fputcsv($output, [$job->job_number, $job->job_date->toDateString(), $job->customer?->legal_name, $job->actual_revenue, $job->actual_cost, $job->profit, $job->status]);
                }
            } else {
                fputcsv($output, ['Unsupported report']);
            }

            fclose($output);
        }, $filename, ['Content-Type' => 'text/csv']);
    }

    private function accountBalances(Request $request, string $from, string $to): Collection
    {
        return Account::query()
            ->where('accounts.company_id', $request->user()->company_id)
            ->leftJoin('journal_lines', 'accounts.id', '=', 'journal_lines.account_id')
            ->leftJoin('journal_entries', function ($join) use ($from, $to, $request): void {
                $join->on('journal_lines.journal_entry_id', '=', 'journal_entries.id')
                    ->where('journal_entries.status', '=', 'posted')
                    ->whereBetween('journal_entries.posting_date', [$from, $to]);

                if ($request->user()->branch_id && ! $request->user()->hasPermission('branches.view_all')) {
                    $join->where('journal_entries.branch_id', '=', $request->user()->branch_id);
                }
            })
            ->select('accounts.id', 'accounts.code', 'accounts.name', 'accounts.type', 'accounts.normal_balance')
            ->selectRaw('COALESCE(SUM(CASE WHEN journal_entries.id IS NOT NULL THEN journal_lines.debit ELSE 0 END),0) debit')
            ->selectRaw('COALESCE(SUM(CASE WHEN journal_entries.id IS NOT NULL THEN journal_lines.credit ELSE 0 END),0) credit')
            ->groupBy('accounts.id', 'accounts.code', 'accounts.name', 'accounts.type', 'accounts.normal_balance')
            ->orderBy('accounts.code')
            ->get();
    }

    private function sumField(iterable $rows, string $field): string
    {
        $total = Decimal::scale('0');
        foreach ($rows as $row) {
            $value = is_array($row) ? ($row[$field] ?? '0') : ($row->{$field} ?? '0');
            $total = Decimal::add($total, (string) $value);
        }

        return $total;
    }

    private function dates(Request $request): array
    {
        return [
            $request->input('from', now()->startOfYear()->toDateString()),
            $request->input('to', today()->toDateString()),
        ];
    }

    private function permission(Request $request): void
    {
        abort_unless($request->user()->hasPermission('reports.view'), 403);
    }

    private function scopeLines(Request $request, $query): void
    {
        if ($request->user()->branch_id && ! $request->user()->hasPermission('branches.view_all')) {
            $query->whereHas('journalEntry', fn ($journal) => $journal->where('branch_id', $request->user()->branch_id));
        }
    }
}
