<?php
declare(strict_types=1);

namespace App\Controllers;

use App\Core\Auth;
use App\Core\Request;
use App\Core\Validator;
use App\Industry\Catalog;
use App\Models\Account;
use App\Models\Activity;
use App\Models\AuditLog;
use App\Models\Opportunity;
use App\Models\PipelineStage;
use App\Models\PortalDocument;
use App\Models\Task;
use App\Models\User;
use App\Services\AutomationService;
use App\Services\CommerceService;
use App\Services\DocumentStore;
use App\Services\IndustryService;
use App\Services\RecordService;
use App\Services\SetupService;

final class OpportunityController extends BaseController
{
    public function index(Request $request): void
    {
        $this->requireModule('opportunities');
        $viewMode = ($_GET['view'] ?? 'kanban') === 'list' ? 'list' : 'kanban';
        $stages = PipelineStage::allActive();
        $accounts = Account::allForSelect();
        $scope = $this->ownerScope();
        $assigned = (int) ($_GET['assigned'] ?? 0);
        $ownerFilter = $scope;
        if ($scope === null && $assigned > 0) {
            $ownerFilter = $assigned;
        }
        $accountId = (int) ($_GET['account'] ?? 0);
        $accountIds = array_map('intval', array_column($accounts, 'id'));
        if ($accountId > 0 && !in_array($accountId, $accountIds, true)) {
            $accountId = 0;
        }
        $stageId = (int) ($_GET['stage'] ?? 0);
        $stageIds = array_map('intval', array_column($stages, 'id'));
        if ($stageId > 0 && !in_array($stageId, $stageIds, true)) {
            $stageId = 0;
        }
        $outcome = (string) ($_GET['outcome'] ?? 'all');
        if (!in_array($outcome, ['all', 'open', 'won', 'lost'], true)) {
            $outcome = 'all';
        }
        $created = (string) ($_GET['created'] ?? 'all');
        if (!in_array($created, ['all', 'today', 'week', 'month', 'last_week', 'last_month'], true)) {
            $created = 'all';
        }
        $close = (string) ($_GET['close'] ?? 'all');
        if (!in_array($close, ['all', 'week', 'month', 'overdue'], true)) {
            $close = 'all';
        }
        $nextStep = (string) ($_GET['next'] ?? 'all');
        if (!in_array($nextStep, ['all', 'overdue', 'today', 'week', 'none'], true)) {
            $nextStep = 'all';
        }
        $probability = (string) ($_GET['probability'] ?? 'all');
        if (!in_array($probability, ['all', 'high', 'medium', 'low'], true)) {
            $probability = 'all';
        }
        $filters = [
            'q' => $this->q(),
            'account_id' => $accountId,
            'stage_id' => $stageId,
            'owner_id' => $ownerFilter,
            'outcome' => $outcome,
            'created' => $created,
            'close' => $close,
            'next_step' => $nextStep,
            'probability' => $probability,
        ];
        $data = [
            'title' => opportunity_label(),
            'stages' => $stages,
            'viewMode' => $viewMode,
            'accounts' => $accounts,
            'owners' => User::salespeople(Auth::instance()->tenantId()),
            'industry' => Catalog::current(),
            'notifications' => $this->notifications(),
            'q' => $this->q(),
            'assigned' => $assigned,
            'showAssigned' => $scope === null,
            'accountId' => $accountId,
            'stageId' => $stageId,
            'outcome' => $outcome,
            'created' => $created,
            'close' => $close,
            'nextStep' => $nextStep,
            'probability' => $probability,
            'counts' => Opportunity::boardCounts($ownerFilter),
        ];
        if ($viewMode === 'list') {
            $page = $this->page();
            $result = Opportunity::paginate($filters, $page, 20);
            $data['rows'] = $result['rows'];
            $data['total'] = $result['total'];
            $data['page'] = $page;
            $data['pages'] = max(1, (int) ceil($result['total'] / 20));
        } else {
            $data['cards'] = Opportunity::kanban($filters);
        }
        view('opportunities/index', $data);
    }

    public function show(Request $request): void
    {
        $this->requireModule('opportunities');
        $id = (int) $request->param('id');
        $opp = Opportunity::detail($id);
        if (!$opp) {
            flash('error', 'Opportunity not found.');
            redirect('opportunities');
        }
        $papers = PortalDocument::papersForOpportunity($id);
        $hasPaper = ['proposal' => false, 'agreement' => false];
        foreach ($papers as $doc) {
            $kind = (string) ($doc['body'] ?? '');
            if ($kind !== 'proposal' && $kind !== 'agreement') {
                $kind = stage_paper_kind(['name' => (string) ($doc['title'] ?? '')]);
            }
            if (isset($hasPaper[$kind])) {
                $hasPaper[$kind] = true;
            }
        }
        view('opportunities/show', [
            'title' => $opp['name'],
            'opp' => $opp,
            'attrs' => IndustryService::decodeAttrs($opp['attrs'] ?? null),
            'industry' => Catalog::current(),
            'stages' => PipelineStage::allActive(),
            'timeline' => Activity::timeline('opportunity', $id),
            'tasks' => Task::forRelated('opportunity', $id),
            'papers' => $papers,
            'hasPaper' => $hasPaper,
            'notifications' => $this->notifications(),
        ]);
    }

    public function store(Request $request): void
    {
        $this->requireModule('opportunities');
        $this->verifyCsrf($request);
        $data = $request->all();
        $v = (new Validator())->required($data, ['name' => 'Name', 'account_id' => 'Account']);
        if ($v->fails()) {
            $this->fail($v);
        }
        $stages = PipelineStage::openStages();
        $stageId = (int) ($data['stage_id'] ?? ($stages[0]['id'] ?? 0));
        $stage = PipelineStage::find($stageId);
        if (!$stage) {
            json_out(['ok' => false, 'error' => 'No pipeline stage is set up.'], 422);
        }
        $id = Opportunity::create([
            'account_id' => (int) $data['account_id'],
            'name' => $data['name'],
            'amount' => (float) ($data['amount'] ?? 0),
            'stage_id' => $stageId,
            'probability' => (int) $stage['probability'],
            'owner_id' => (int) ($data['owner_id'] ?: Auth::instance()->id()),
            'next_step' => $data['next_step'] ?? 'Follow up',
            'next_step_at' => $data['next_step_at'] ?: date('Y-m-d H:i:s', strtotime('+2 days')),
            'close_date' => $data['close_date'] ?: null,
            'attrs' => json_encode(IndustryService::sanitizeAttrs($data['attrs'] ?? [], entity_fields('opportunity')), JSON_UNESCAPED_UNICODE),
        ]);
        RecordService::logActivity('note', 'Opportunity created', null, 'opportunity', $id);
        AuditLog::write('opportunity.create', 'opportunity', $id);
        json_out(['ok' => true, 'id' => $id, 'redirect' => url('opportunities/' . $id)]);
    }

    public function moveStage(Request $request): void
    {
        $this->requireModule('opportunities');
        $this->verifyCsrf($request);
        $id = (int) $request->param('id');
        $opp = Opportunity::find($id);
        if (!$opp) {
            json_out(['ok' => false, 'error' => 'Not found.'], 404);
        }
        $stage = PipelineStage::find((int) $request->input('stage_id'));
        if (!$stage) {
            json_out(['ok' => false, 'error' => 'Invalid stage.'], 422);
        }
        $needed = SetupService::stageRequired((string) ($stage['code'] ?? ''));
        if ($needed === []) {
            $needed = SetupService::stageRequired((int) $stage['id']);
        }
        if ($needed !== []) {
            $attrs = IndustryService::decodeAttrs($opp['attrs'] ?? null);
            $posted = $request->all();
            foreach ($needed as $key) {
                $val = $posted[$key] ?? ($posted['attrs'][$key] ?? ($opp[$key] ?? ($attrs[$key] ?? '')));
                if ($val === null || (is_string($val) && trim($val) === '')) {
                    json_out(['ok' => false, 'error' => 'Fill required boxes before moving to ' . $stage['name'] . '.'], 422);
                }
            }
        }
        $patch = [
            'stage_id' => (int) $stage['id'],
            'probability' => (int) $stage['probability'],
        ];
        if ((int) $stage['is_lost'] === 1) {
            $reason = trim((string) $request->input('win_loss_reason', ''));
            if ($reason === '') {
                json_out(['ok' => false, 'error' => 'Enter a lost reason.', 'need_reason' => true], 422);
            }
            $patch['win_loss_reason'] = $reason;
        }
        if ((int) $stage['is_won'] === 1) {
            $patch['win_loss_reason'] = $request->input('win_loss_reason') ?: 'Won';
            $patch['close_date'] = date('Y-m-d');
        }
        if ($request->input('next_step')) {
            $patch['next_step'] = $request->input('next_step');
        }
        if ($request->input('next_step_at')) {
            $patch['next_step_at'] = $request->input('next_step_at');
        }
        $paperKind = stage_paper_kind($stage);
        if ($paperKind !== '') {
            $this->saveStagePaper($opp, $paperKind, (int) $opp['stage_id'] !== (int) $stage['id']);
        }
        Opportunity::update($id, $patch);
        RecordService::logActivity('stage', 'Moved to ' . $stage['name'], $patch['win_loss_reason'] ?? null, 'opportunity', $id);
        AuditLog::write('opportunity.stage', 'opportunity', $id, ['stage' => $stage['name']]);
        $ctx = [
            'owner_id' => (int) ($opp['owner_id'] ?? 0),
            'related_type' => 'opportunity',
            'related_id' => $id,
            'title' => (string) ($opp['name'] ?? $stage['name']),
        ];
        AutomationService::run('deal_stage_changed', $ctx);
        if ((int) $stage['is_won'] === 1) {
            AutomationService::run('deal_won', $ctx);
            $fresh = Opportunity::find($id) ?: $opp;
            $made = CommerceService::fromWonDeal($fresh);
            if ($made !== []) {
                RecordService::logActivity('note', 'Created from won deal', implode(', ', $made), 'opportunity', $id);
            }
        }
        json_out(['ok' => true, 'stage' => $stage['name']]);
    }

    public function update(Request $request): void
    {
        $this->requireModule('opportunities');
        $this->verifyCsrf($request);
        $id = (int) $request->param('id');
        if (!Opportunity::find($id)) {
            json_out(['ok' => false, 'error' => 'Not found.'], 404);
        }
        $data = $request->all();
        Opportunity::update($id, [
            'name' => $data['name'],
            'amount' => (float) ($data['amount'] ?? 0),
            'next_step' => $data['next_step'] ?? null,
            'next_step_at' => $data['next_step_at'] ?: null,
            'close_date' => $data['close_date'] ?: null,
            'attrs' => json_encode(IndustryService::sanitizeAttrs($data['attrs'] ?? [], entity_fields('opportunity')), JSON_UNESCAPED_UNICODE),
        ]);
        json_out(['ok' => true]);
    }

    public function fulfill(Request $request): void
    {
        $this->requireModule('opportunities');
        $this->verifyCsrf($request);
        $id = (int) $request->param('id');
        $opp = Opportunity::find($id);
        if (!$opp) {
            json_out(['ok' => false, 'error' => 'Not found.'], 404);
        }
        $made = CommerceService::fromWonDeal($opp);
        if ($made === []) {
            json_out(['ok' => true, 'message' => 'This deal already has the matching quote, order, contract, or job.']);
        }
        RecordService::logActivity('note', 'Created from deal', implode(', ', $made), 'opportunity', $id);
        json_out(['ok' => true, 'message' => 'Created ' . implode(', ', $made) . ' from this customer.']);
    }

    /** @param array<string, mixed> $opp */
    private function saveStagePaper(array $opp, string $kind, bool $entering): void
    {
        $label = stage_paper_label($kind);
        if ($label === '') {
            return;
        }
        $file = $_FILES['paper'] ?? [];
        $hasUpload = is_array($file) && (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE;
        $existing = PortalDocument::latestPaper((int) $opp['id'], $kind);
        if (!$hasUpload) {
            if ($existing) {
                return;
            }
            json_out([
                'ok' => false,
                'error' => $entering
                    ? 'Upload the ' . strtolower($label) . ' PDF to move to ' . $label . '.'
                    : 'Upload the ' . strtolower($label) . ' PDF for this stage.',
                'need_paper' => $kind,
            ], 422);
        }
        $accountId = (int) ($opp['account_id'] ?? 0);
        if ($accountId < 1) {
            json_out(['ok' => false, 'error' => 'This deal needs an account before you can attach a PDF.'], 422);
        }
        try {
            $name = DocumentStore::accept($file, 'Upload the ' . strtolower($label) . ' as a PDF.');
        } catch (\RuntimeException $e) {
            json_out(['ok' => false, 'error' => $e->getMessage()], 422);
        }
        $docId = PortalDocument::create([
            'account_id' => $accountId,
            'owner_id' => (int) Auth::instance()->id(),
            'related_type' => 'opportunity',
            'related_id' => (int) $opp['id'],
            'title' => $label . ' - ' . mb_substr((string) ($opp['name'] ?? $label), 0, 140),
            'kind' => 'file',
            'body' => $kind,
            'file_name' => $name,
            'shared' => 1,
        ]);
        try {
            DocumentStore::save($docId, (string) $file['tmp_name']);
        } catch (\RuntimeException $e) {
            json_out(['ok' => false, 'error' => $e->getMessage()], 500);
        }
        RecordService::logActivity('note', $label . ' uploaded', $name, 'opportunity', (int) $opp['id']);
        AuditLog::write('document.share', 'portal_document', $docId, ['kind' => $kind]);
    }
}
