<?php

namespace App\Jobs\Synapse;

use App\Models\Communication\Call;
use App\Models\Communication\Recording;
use App\Models\Synapse\CallScore;
use App\Models\Synapse\DataProtectionAudit;
use App\Models\Synapse\SynapseTranscript;
use App\Models\Task;
use App\Synapse\Agents\CallClassifierAgent;
use App\Synapse\Agents\CallScorerAgent;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class ClassifyAndScoreCallJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries   = 2;

    public int $timeout = 300;  // classifier (~10s) + scorer (~120s) + headroom

    public int $backoff = 30;

    public function __construct(
        public readonly int $recordingId,
        public readonly bool $force = false,
    ) {}

    public function handle(): void
    {
        $recording = Recording::with(['communication', 'call'])->find($this->recordingId);

        if (! $recording) {
            Log::warning("ClassifyAndScoreCallJob: recording {$this->recordingId} not found.");

            return;
        }

        // Institution and ad-hoc calls are transcribed for the record but are not
        // client compliance interactions — never compliance-score them. This guard
        // sits in the job (not at a dispatch site) so it covers every entry point:
        // the post-transcription dispatch, the manual "Re-analyse" button, and the
        // wrap-up "initial call" re-score.
        if (($recording->call?->call_type ?? 'client') !== 'client') {
            Log::info('ClassifyAndScoreCallJob: skipping non-client call (call_type='.($recording->call?->call_type ?? 'null').") for recording {$this->recordingId}.");

            return;
        }

        $transcript = SynapseTranscript::where('recording_id', $this->recordingId)
            ->where('status', 'completed')
            ->first();

        if (! $transcript || blank($transcript->formatted_transcript)) {
            Log::warning("ClassifyAndScoreCallJob: no completed transcript for recording {$this->recordingId}.");

            return;
        }

        // An agent can mark a call as the first substantive client conversation at
        // wrap-up. That's an authoritative signal: score it directly (skip the
        // classifier below) and don't let the short-call guard discard it.
        $markedInitial = $recording->call?->disposition === Call::DISPOSITION_INITIAL_CALL;
        $force = $this->force || $markedInitial;

        // Keep the legacy short-call guard for calls outside the lightweight
        // data-protection audit. Inbound and successfully-connected calls still
        // need classification, including a short call which should legitimately fail.
        $wordCount = str_word_count(strip_tags($transcript->formatted_transcript));
        $dataProtectionCandidate = $recording->communication?->direction === 'inbound'
            || $recording->call?->disposition === 'connected_successfully';

        if (! $force
            && ! $dataProtectionCandidate
            && (($recording->duration !== null && $recording->duration < 30) || $wordCount < 15)) {
            Log::info("ClassifyAndScoreCallJob: skipping recording {$this->recordingId} — too short to classify (duration={$recording->duration}s, words={$wordCount}).");

            return;
        }

        $score = CallScore::updateOrCreate(
            ['recording_id' => $this->recordingId],
            [
                'solicitor_id'     => $recording->solicitor_id,
                'communication_id' => $recording->communication_id,
                'call_id'          => $recording->call_id,
                'status'           => 'processing',
                'error'            => null,
            ]
        );

        try {
            if ($markedInitial) {
                // Agent flagged this as an initial call at wrap-up — trust that and
                // score directly rather than asking the classifier.
                $classification = 'initial';
                Log::debug("ClassifyAndScoreCallJob: recording {$this->recordingId} marked 'initial' by agent disposition; skipping classifier.");
            } else {
                $classifierPrompt = "CALL TRANSCRIPT:\n\n".$transcript->formatted_transcript;
                $classification   = (new CallClassifierAgent)->prompt($classifierPrompt)['classification'] ?? 'not_applicable';

                Log::debug("ClassifyAndScoreCallJob: classification={$classification} for recording {$this->recordingId}");
            }

            if ($classification === 'not_applicable') {
                $this->excludeExistingDataProtectionAudit();

                $score->update([
                    'is_initial_call' => null,
                    'status'          => 'completed',
                ]);
                return;
            }

            $isInitial = $classification === 'initial';

            if (!$isInitial) {
                $score->update([
                    'is_initial_call' => false,
                    'status'          => 'completed',
                ]);

                AuditDataProtectionCheckJob::dispatch($this->recordingId, $classification);

                return;
            }

            $this->excludeExistingDataProtectionAudit();

            $scorerPrompt = "CALL TRANSCRIPT:\n\n".$transcript->formatted_transcript;
            $scorerResponse = (new CallScorerAgent)->prompt($scorerPrompt);
            $data = $scorerResponse->structured;

            Log::debug("ClassifyAndScoreCallJob: scorer returned for recording {$this->recordingId}.", [
                'total_score' => $data['total_score'] ?? null,
                'risk_rating' => $data['risk_rating'] ?? null,
                'has_sections'=> isset($data['sections']),
            ]);

            $totalScore = (int) $data['total_score'];
            $riskRating = $this->resolveRiskRating($totalScore);

            $score->update([
                'is_initial_call'    => true,
                'status'             => 'completed',
                'total_score'        => $totalScore,
                'risk_rating'        => $riskRating,
                'passed'             => ($data['passed'] ?? false) && $riskRating !== 'high_risk',
                'auto_fail'          => (bool) ($data['auto_fail'] ?? false),
                'auto_fail_triggers' => $data['auto_fail_triggers'] ?? [],
                'section_scores'     => $data['sections'] ?? null,
                'summary'            => $data['summary'] ?? null,
                'raw_output'         => $scorerResponse->toJson(),
            ]);

        } catch (\Throwable $e) {
            $score->update([
                'status' => 'failed',
                'error'  => $e->getMessage(),
            ]);

            Log::error("ClassifyAndScoreCallJob: failed for recording {$this->recordingId}.", [
                'error' => $e->getMessage(),
            ]);

            report($e);
            $this->fail($e);
        }
    }

    private function resolveRiskRating(int $score): string
    {
        return match (true) {
            $score >= 90 => 'low_risk',
            $score >= 80 => 'acceptable',
            $score >= 70 => 'compliance_concern',
            default      => 'high_risk',
        };
    }

    private function excludeExistingDataProtectionAudit(): void
    {
        $audit = DataProtectionAudit::where('recording_id', $this->recordingId)->first();

        if (! $audit) {
            return;
        }

        if ($audit->task_id) {
            Task::query()
                ->whereKey($audit->task_id)
                ->whereNull('completed_at')
                ->update(['completed_at' => now()]);
        }

        $audit->update(['status' => 'excluded']);
    }
}
