<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

/**
 * A chat conversation / send-queue. The hot operations are:
 *   - list (filtered by status, search, sort)
 *   - open (load messages)
 *   - send (append a message + bump preview/last_message_at)
 *   - archive / unarchive / destroy
 *
 * Use the provided scopes (`forUser`, `notArchived`, `withStatus`,
 * `search`, `sorted`) instead of repeating the same where-clauses
 * across the controller.
 */
class Conversation extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id', 'workspace_id',
        'device_id', 'contact_group_id',
        'title', 'preview',
        'status', 'platform', 'provider', 'archived', 'origin',
        'recipients_count', 'last_message_at', 'scheduled_at', 'scheduled_timezone', 'raw_jid', 'alt_jid',
        // inbox lifecycle (separate from send-queue `status`)
        'inbox_status', 'priority',
        'assignee_user_id', 'assignee_team_id', 'assignee_agent_id',
        'first_response_at', 'snoozed_until', 'resolved_at', 'resolved_by', 'resolved_by_agent_id',
        'sla_policy_id', 'sla_first_response_due', 'sla_resolution_due', 'sla_breached',
        'channel', 'is_spam', 'routing_meta',
        'last_inbound_at', 'last_outbound_at', 'unread_count',
        'escalation_due_at', 'escalation_action',
    ];

    protected $casts = [
        // title/preview are PII the operator wrote (queue label,
        // last-message snippet) — encrypt them at rest the same
        // way the Contact model encrypts name/mobile/email.
        'title'            => 'encrypted',
        'preview'          => 'encrypted',
        'archived'         => 'boolean',
        'recipients_count' => 'integer',
        'last_message_at'  => 'datetime',
        'scheduled_at'     => 'datetime',
        // inbox lifecycle
        'first_response_at'      => 'datetime',
        'snoozed_until'          => 'datetime',
        'resolved_at'            => 'datetime',
        'sla_first_response_due' => 'datetime',
        'sla_resolution_due'     => 'datetime',
        'sla_breached'           => 'boolean',
        'is_spam'                => 'boolean',
        'routing_meta'           => 'array',
        'last_inbound_at'        => 'datetime',
        'last_outbound_at'       => 'datetime',
        'unread_count'           => 'integer',
        'escalation_due_at'      => 'datetime',
        'escalation_action'      => 'array',
    ];

    public const STATUSES        = ['pending', 'sent', 'failed', 'scheduled'];
    public const INBOX_STATUSES  = ['open', 'pending', 'snoozed', 'resolved', 'closed', 'spam'];
    public const PRIORITIES      = ['low', 'normal', 'high', 'urgent'];

    public function messages(): HasMany
    {
        return $this->hasMany(Message::class)->orderBy('created_at');
    }

    /**
     * Team-inbox bubbles live in their own table (`inbox_messages`)
     * so the inbox UI isn't mixed up with campaign / broadcast rows.
     * Use this relation everywhere team-inbox code path — list view,
     * thread show, per-message actions, AI agent attribution.
     */
    public function inboxMessages(): HasMany
    {
        return $this->hasMany(InboxMessage::class)->orderBy('created_at');
    }

    public function latestMessage(): HasMany
    {
        return $this->hasMany(Message::class)->latestOfMany();
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class)->withDefault();
    }

    public function group(): BelongsTo
    {
        return $this->belongsTo(ContactGroup::class, 'contact_group_id')->withDefault();
    }

    public function scopeForUser(Builder $q, ?int $userId): Builder
    {
        return $userId ? $q->where('user_id', $userId) : $q;
    }

    /**
     * Scope to the conversations owned by the authenticated user
     * AND living in the workspace they currently have selected from
     * the top-bar switcher. Use this in place of `forUser(Auth::id())`
     * on /chat and any other personal-inbox surface so the row set
     * actually changes when the workspace flips.
     *
     * Legacy rows with NULL `workspace_id` fall back to plain user
     * ownership — protects against pre-multi-workspace conversations
     * that haven't been backfilled yet.
     */
    public function scopeForCurrentWorkspace(Builder $q): Builder
    {
        $user = auth()->user();
        if (!$user) return $q->whereRaw('1=0');
        $uId  = (int) $user->id;
        $wsId = (int) ($user->current_workspace_id ?? 0);

        // Workspace-shared visibility: every conversation in the
        // current workspace, regardless of which teammate created it.
        // Mike (member of workspace Acme) should see conversations
        // Sara started in Acme. `user_id` is just a "who started it"
        // record — not a visibility filter.
        //
        // Legacy fallback: rows with NULL workspace_id (pre-multi-
        // workspace migration) stay visible to their original owner
        // only, so they're not lost while we backfill.
        return $q->where(function ($qq) use ($wsId, $uId) {
            $qq->where('workspace_id', $wsId)
               ->orWhere(function ($qqq) use ($uId) {
                   $qqq->whereNull('workspace_id')->where('user_id', $uId);
               });
        });
    }

    /**
     * Limit to conversations created by the chat composer (the user-facing
     * inbox) — i.e. exclude rows the campaign dispatcher created. /chat
     * uses this; /wa-campaigns/{id} doesn't care because it reads from
     * the WpCampaignContact log instead.
     */
    public function scopeChatOnly(Builder $q): Builder
    {
        return $q->where('origin', 'chat');
    }

    /**
     * Scope conversations to a specific engine via the canonical
     * `provider` column ('waba' | 'baileys' | 'twilio'). Added by
     * migration 2026_05_26_140000 alongside a backfill of every
     * existing row to 'baileys'. The legacy `platform` column ('W' /
     * 'WB' / 'T') was historically mis-stamped — all Baileys + WABA
     * inbound got 'W' regardless — so it can't be trusted here.
     */
    public function scopeForEngine(Builder $q, string $engine): Builder
    {
        return $q->where('provider', $engine);
    }

    /**
     * Scope to the engine the current workspace is actually on. Use
     * this in any list view (team-inbox queue, dashboard live inbox,
     * unread summary) so switching the workspace's engine instantly
     * hides conversations that belong to the other engine. Falls
     * back to no-op when there's no auth context (queue jobs).
     */
    public function scopeForCurrentEngine(Builder $q): Builder
    {
        $u   = auth()->user();
        $wsId = (int) ($u?->current_workspace_id ?? 0);
        if (!$wsId) return $q;
        $engine = \App\Services\WorkspaceEngine::for($wsId);
        return $q->where('provider', $engine);
    }

    public function scopeForWorkspace(Builder $q, ?int $workspaceId): Builder
    {
        return $workspaceId ? $q->where('workspace_id', $workspaceId) : $q;
    }

    public function scopeAssignedTo(Builder $q, int $userId): Builder
    {
        return $q->where('assignee_user_id', $userId);
    }

    public function scopeUnassigned(Builder $q): Builder
    {
        return $q->whereNull('assignee_user_id');
    }

    public function scopeWithInboxStatus(Builder $q, string|array $status): Builder
    {
        return is_array($status) ? $q->whereIn('inbox_status', $status) : $q->where('inbox_status', $status);
    }

    public function scopeOpen(Builder $q): Builder
    {
        return $q->whereIn('inbox_status', ['open', 'pending']);
    }

    public function scopeSlaBreached(Builder $q): Builder
    {
        return $q->where('sla_breached', true);
    }

    public function scopeForTeam(Builder $q, int $teamId): Builder
    {
        return $q->where('assignee_team_id', $teamId);
    }

    public function workspace(): BelongsTo
    {
        return $this->belongsTo(Workspace::class);
    }

    public function assignee(): BelongsTo
    {
        return $this->belongsTo(User::class, 'assignee_user_id')->withDefault();
    }

    public function team(): BelongsTo
    {
        return $this->belongsTo(Team::class, 'assignee_team_id')->withDefault();
    }

    public function agent(): BelongsTo
    {
        return $this->belongsTo(\App\Models\AiAgent::class, 'assignee_agent_id')->withDefault();
    }

    public function notes(): HasMany
    {
        return $this->hasMany(ConversationNote::class)->orderByDesc('created_at');
    }

    public function events(): HasMany
    {
        return $this->hasMany(ConversationEvent::class)->orderByDesc('created_at');
    }

    public function participants(): HasMany
    {
        return $this->hasMany(ConversationParticipant::class);
    }

    public function tags()
    {
        return $this->belongsToMany(Tag::class, 'conversation_tag')->withTimestamps()->withPivot('added_by');
    }

    public function scopeNotArchived(Builder $q): Builder
    {
        return $q->where('archived', false);
    }

    public function scopeArchived(Builder $q): Builder
    {
        return $q->where('archived', true);
    }

    public function scopeWithStatus(Builder $q, string $status): Builder
    {
        return $q->where('status', $status);
    }

    /**
     * Search is intentionally NOT a SQL scope because the columns it
     * looks at (title / preview) are encrypted-at-rest — `LIKE` over
     * ciphertext matches nothing. Callers should hydrate the
     * collection (with the SQL-side filters applied) and then run
     * Conversation::filterBySearch($items, $term) to do a plaintext
     * pass in PHP.
     */
    public static function filterBySearch($items, ?string $term)
    {
        $term = mb_strtolower(trim((string) $term));
        if ($term === '') {
            return $items;
        }
        return $items->filter(function (self $c) use ($term) {
            return str_contains(mb_strtolower((string) $c->title),   $term)
                || str_contains(mb_strtolower((string) $c->preview), $term);
        })->values();
    }

    /**
     * SQL-side sort. `title` is encrypted (non-deterministic
     * ciphertext) so `name-asc` / `name-desc` would order on
     * ciphertext if we let the DB handle them — we apply those in
     * PHP via sortBySorted() after hydration.
     */
    public function scopeSorted(Builder $q, ?string $sort): Builder
    {
        // `name-asc` / `name-desc` deliberately fall through to the
        // default date-desc — once rows are decrypted in PHP,
        // sortByKey() will reorder them by title.
        return $sort === 'date-asc'
            ? $q->orderBy('last_message_at')->orderBy('id')
            : $q->orderByDesc('last_message_at')->orderByDesc('id');
    }

    /**
     * Apply name-based sort in PHP (after rows are decrypted into
     * the collection). For date sorts the DB has already done the
     * right thing, so this is a no-op for those.
     */
    public static function sortByKey($items, ?string $sort)
    {
        return match ($sort) {
            'name-asc'  => $items->sortBy(fn ($c)        => mb_strtolower((string) $c->title))->values(),
            'name-desc' => $items->sortByDesc(fn ($c)    => mb_strtolower((string) $c->title))->values(),
            default     => $items,
        };
    }

    /**
     * Filter to one of: all, scheduled, archived, sent, pending, failed.
     */
    public function scopeFiltered(Builder $q, ?string $filter): Builder
    {
        return match ($filter) {
            'archived'  => $q->archived(),
            'scheduled' => $q->notArchived()->withStatus('scheduled'),
            'sent'      => $q->notArchived()->withStatus('sent'),
            'pending'   => $q->notArchived()->withStatus('pending'),
            'failed'    => $q->notArchived()->withStatus('failed'),
            default     => $q->notArchived(),
        };
    }
}
