<?php

namespace App\Http\Controllers\Api\Mobile;

use App\Http\Controllers\Controller;
use App\Models\Attachment;
use App\Models\Campaign;
use App\Models\EmailAccount;
use App\Services\Campaign\ABTestService;
use App\Services\Campaign\CampaignService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

/**
 * Mobile campaign endpoints.
 *
 * Mirrors the existing Api\CampaignController but with a payload shape
 * tuned for the Flutter screens (flat top-level keys, pre-computed
 * derived metrics like open_rate / click_rate), and routed under the
 * /mobile prefix so the throttle group + Sanctum auth match the rest
 * of the mobile namespace.
 *
 * Endpoints:
 *   GET    /campaigns              list with status + type filters + search
 *   POST   /campaigns              create as draft
 *   GET    /campaigns/{id}         detail with computed stats
 *   PUT    /campaigns/{id}         update (only draft or scheduled)
 *   POST   /campaigns/{id}/send    kick off send (CampaignService)
 *   DELETE /campaigns/{id}         soft delete (anything but 'sending')
 */
class CampaignsController extends Controller
{
    public function __construct(
        private readonly CampaignService $campaignService,
        private readonly ABTestService $abTestService,
    ) {}

    /**
     * GET /api/v1/mobile/campaigns
     * Query: ?status=draft&type=regular&search=foo&page=1&per_page=25
     */
    public function index(Request $request): JsonResponse
    {
        $workspaceId = $request->user()->active_workspace_id;
        if (!$workspaceId) {
            return response()->json(['data' => [], 'meta' => $this->emptyMeta()]);
        }

        $q = Campaign::query()
            ->where('workspace_id', $workspaceId)
            ->with('createdBy:id,name');

        if ($status = $request->query('status')) {
            $q->where('status', $status);
        }
        if ($type = $request->query('type')) {
            $q->where('type', $type);
        }
        if ($channel = $request->query('channel')) {
            // Channel column is nullable for email campaigns — match
            // either null OR the requested channel literal so a Flutter
            // filter chip for "email" returns historical rows too.
            if ($channel === 'email') {
                $q->where(function ($w) { $w->where('channel', 'email')->orWhereNull('channel'); });
            } else {
                $q->where('channel', $channel);
            }
        }
        if ($search = trim((string) $request->query('search', ''))) {
            $like = '%' . $search . '%';
            $q->where(function ($w) use ($like) {
                $w->where('name', 'like', $like)->orWhere('subject', 'like', $like);
            });
        }

        $sortBy  = $request->query('sort_by', 'created_at');
        $sortDir = $request->query('sort_dir', 'desc');
        if (!in_array($sortBy, ['name', 'status', 'recipients_count', 'created_at', 'sent_at', 'scheduled_at'], true)) {
            $sortBy = 'created_at';
        }
        $q->orderBy($sortBy, $sortDir === 'asc' ? 'asc' : 'desc');

        $perPage = min(max((int) $request->query('per_page', 25), 1), 100);
        $paginator = $q->paginate($perPage);

        return response()->json([
            'data' => collect($paginator->items())->map(fn (Campaign $c) => $this->shapeRow($c))->all(),
            'meta' => [
                'current_page' => $paginator->currentPage(),
                'per_page'     => $paginator->perPage(),
                'total'        => $paginator->total(),
                'has_more'     => $paginator->hasMorePages(),
            ],
        ]);
    }

    /**
     * GET /api/v1/mobile/campaigns/{id}
     * Detail + computed open/click/bounce rates.
     */
    public function show(Request $request, int $id): JsonResponse
    {
        $workspaceId = $request->user()->active_workspace_id;

        $campaign = Campaign::with(
                'createdBy:id,name',
                'emailAccount:id,display_name,email,provider',
                'attachments',
                'abTestVariants',
            )
            ->where('workspace_id', $workspaceId)
            ->find($id);

        if (!$campaign) {
            return response()->json(['message' => 'Campaign not found.'], 404);
        }

        // Opportunistic finalize. The campaign should flip from
        // 'sending' → 'sent' when the last SendCampaignEmailJob calls
        // checkAndCompleteIfDone(). But that requires the queue worker
        // to actually be running and reach the final job. Many self-
        // hosted installs run `QUEUE_CONNECTION=sync` (no worker), and
        // for those the per-recipient jobs ran inline during the
        // /send request — yet the campaign can still get stuck at
        // 'sending' if the very last job lost an atomic race or if
        // a worker died mid-batch.
        //
        // Whenever the mobile app polls the campaign detail, take the
        // opportunity to re-check. It's a single COUNT query in the
        // happy path (sent campaigns return immediately), and the
        // atomic UPDATE inside checkAndCompleteIfDone prevents any
        // double-flip if a worker beats us to it.
        if ($campaign->status === 'sending') {
            $this->campaignService->checkAndCompleteIfDone($campaign);
            $campaign = $campaign->fresh([
                'createdBy:id,name',
                'emailAccount:id,display_name,email,provider',
                'attachments',
                'abTestVariants',
            ]);
        }

        return response()->json([
            'data' => $this->shapeRow($campaign, full: true),
        ]);
    }

    /**
     * POST /api/v1/mobile/campaigns
     * Body for an email broadcast:
     *   {
     *     name, type=regular|drip|ab_test, subject, body_html,
     *     preview_text?, audience_type=all|segment|list, audience_id?,
     *     email_account_id?, scheduled_at? (ISO8601)
     *   }
     * Body for an SMS broadcast:
     *   { name, type, channel='sms', from_number, body_text,
     *     audience_type, audience_id?, scheduled_at? }
     */
    public function store(Request $request): JsonResponse
    {
        $workspaceId = $request->user()->active_workspace_id;
        if (!$workspaceId) {
            return response()->json(['message' => 'No active workspace.'], 422);
        }

        $data = Validator::make($request->all(), [
            'name'             => ['required', 'string', 'max:255'],
            'type'             => ['required', 'string', 'in:regular,drip,ab_test'],
            'channel'          => ['nullable', 'string', 'in:email,sms'],
            'subject'          => ['nullable', 'string', 'max:500'],
            'body_html'        => ['nullable', 'string'],
            'body_json'        => ['nullable', 'array'],
            'body_text'        => ['nullable', 'string'],
            'preview_text'     => ['nullable', 'string', 'max:255'],
            'audience_type'    => ['nullable', 'string', 'in:all,segment,list'],
            'audience_id'      => ['nullable', 'integer'],
            'from_number'      => ['nullable', 'string', 'max:50'],
            'email_account_id' => ['nullable', 'integer', Rule::exists('email_accounts', 'id')->where('workspace_id', $workspaceId)],
            'scheduled_at'     => ['nullable', 'date', 'after:now'],
            // Attach previously-uploaded files in one shot — the Flutter
            // composer uploads via /campaigns/{id}/attachments first
            // (which keys them by campaign_id), then sends the id list
            // here when forking a draft from a template.
            'attachment_ids'         => ['nullable', 'array'],
            'attachment_ids.*'       => ['integer'],
            // AB test variants — only consumed when type=ab_test. Mobile
            // can either pass them inline at create time OR call the
            // /variants endpoints after the draft exists.
            'variants'               => ['nullable', 'array', 'min:2', 'max:10'],
            'variants.*.variant'     => ['required_with:variants', 'string', 'max:5'],
            'variants.*.subject'     => ['required_with:variants', 'string', 'max:500'],
            'variants.*.body_html'   => ['nullable', 'string'],
            'variants.*.percentage'  => ['required_with:variants', 'integer', 'min:1', 'max:100'],
        ])->validate();

        // Split out fields the Campaign model doesn't have — they're
        // applied through side effects (attachment relink, AB setup).
        $attachmentIds = $data['attachment_ids'] ?? [];
        $variants = $data['variants'] ?? [];
        unset($data['attachment_ids'], $data['variants']);

        $campaign = Campaign::create(array_merge(
            $data,
            [
                'workspace_id' => $workspaceId,
                'created_by'   => $request->user()->id,
                'status'       => 'draft',
            ],
        ));

        $this->linkAttachments($campaign, $attachmentIds, $workspaceId);

        if ($campaign->type === 'ab_test' && !empty($variants)) {
            try {
                $this->abTestService->setupTest($campaign, $variants);
            } catch (\InvalidArgumentException $e) {
                return response()->json([
                    'message' => $e->getMessage(),
                    'data'    => $this->shapeRow($campaign->refresh(), full: true),
                ], 422);
            }
            $campaign->refresh();
        }

        $campaign->load('createdBy:id,name', 'emailAccount:id,display_name,email,provider', 'attachments', 'abTestVariants');

        return response()->json([
            'message' => __('Campaign saved as draft.'),
            'data'    => $this->shapeRow($campaign, full: true),
        ], 201);
    }

    /**
     * PUT /api/v1/mobile/campaigns/{id}
     * Editable only while status is draft / scheduled. A sent or
     * sending campaign is frozen — emitting a 422 makes the Flutter
     * UI surface a clear error instead of silently dropping the edit.
     */
    public function update(Request $request, int $id): JsonResponse
    {
        $workspaceId = $request->user()->active_workspace_id;

        $campaign = Campaign::where('workspace_id', $workspaceId)->find($id);
        if (!$campaign) {
            return response()->json(['message' => 'Campaign not found.'], 404);
        }

        if (!in_array($campaign->status, ['draft', 'scheduled'], true)) {
            return response()->json([
                'message' => "Cannot edit a campaign with status '{$campaign->status}'. Only drafts and scheduled campaigns can be modified.",
            ], 422);
        }

        $data = Validator::make($request->all(), [
            'name'             => ['sometimes', 'required', 'string', 'max:255'],
            'type'             => ['sometimes', 'required', 'string', 'in:regular,drip,ab_test'],
            'channel'          => ['sometimes', 'nullable', 'string', 'in:email,sms'],
            'subject'          => ['sometimes', 'nullable', 'string', 'max:500'],
            'body_html'        => ['sometimes', 'nullable', 'string'],
            'body_json'        => ['sometimes', 'nullable', 'array'],
            'body_text'        => ['sometimes', 'nullable', 'string'],
            'preview_text'     => ['sometimes', 'nullable', 'string', 'max:255'],
            'audience_type'    => ['sometimes', 'nullable', 'string', 'in:all,segment,list'],
            'audience_id'      => ['sometimes', 'nullable', 'integer'],
            'from_number'      => ['sometimes', 'nullable', 'string', 'max:50'],
            'email_account_id' => ['sometimes', 'nullable', 'integer', Rule::exists('email_accounts', 'id')->where('workspace_id', $workspaceId)],
            'scheduled_at'     => ['sometimes', 'nullable', 'date', 'after:now'],
            // Same shapes as store(). Sending an empty array detaches all
            // attachments / clears all variants — useful when the editor
            // resets the section.
            'attachment_ids'        => ['sometimes', 'array'],
            'attachment_ids.*'      => ['integer'],
            'variants'              => ['sometimes', 'array', 'min:2', 'max:10'],
            'variants.*.variant'    => ['required_with:variants', 'string', 'max:5'],
            'variants.*.subject'    => ['required_with:variants', 'string', 'max:500'],
            'variants.*.body_html'  => ['nullable', 'string'],
            'variants.*.percentage' => ['required_with:variants', 'integer', 'min:1', 'max:100'],
        ])->validate();

        $attachmentIds = $data['attachment_ids'] ?? null;
        $variants = $data['variants'] ?? null;
        unset($data['attachment_ids'], $data['variants']);

        $campaign->update($data);

        // Only re-link when the caller actually sent the key — distinct
        // from "send an empty array" which detaches everything.
        if (is_array($attachmentIds)) {
            $this->linkAttachments($campaign, $attachmentIds, $workspaceId);
        }

        if (is_array($variants) && !empty($variants)) {
            try {
                $this->abTestService->setupTest($campaign, $variants);
            } catch (\InvalidArgumentException $e) {
                return response()->json([
                    'message' => $e->getMessage(),
                    'data'    => $this->shapeRow($campaign->refresh(), full: true),
                ], 422);
            }
            $campaign->refresh();
        }

        $campaign->load('createdBy:id,name', 'emailAccount:id,display_name,email,provider', 'attachments', 'abTestVariants');

        return response()->json([
            'message' => __('Campaign updated.'),
            'data'    => $this->shapeRow($campaign, full: true),
        ]);
    }

    /**
     * Attach a set of previously-uploaded Attachment rows to this
     * campaign. Used by both store() and update().
     *
     * Semantics:
     *   - Workspace-scoped lookup — rejects cross-tenant attachment IDs
     *     silently (returns false for those rows) rather than 422-ing
     *     so a stale Flutter cache doesn't break the whole save.
     *   - Sets campaign_id on each row + nulls message_id. This is the
     *     "claim" step the upload endpoint leaves to whoever links
     *     the file to a campaign.
     *   - Detach behaviour: any current campaign attachment NOT in the
     *     new id list is unlinked (campaign_id set to NULL) AND its
     *     storage file deleted. Without the file delete the disk
     *     fills with orphans every time the editor reshapes the list.
     */
    private function linkAttachments(Campaign $campaign, array $ids, int $workspaceId): void
    {
        $current = $campaign->attachments()->get();

        // Detach + delete files for attachments removed from the list.
        foreach ($current as $att) {
            if (in_array($att->id, $ids, true)) continue;
            if ($att->storage_path && \Illuminate\Support\Facades\Storage::disk('local')->exists($att->storage_path)) {
                \Illuminate\Support\Facades\Storage::disk('local')->delete($att->storage_path);
            }
            $att->delete();
        }

        // Claim newly-added ids.
        if (!empty($ids)) {
            Attachment::where('workspace_id', $workspaceId)
                ->whereIn('id', $ids)
                ->update(['campaign_id' => $campaign->id, 'message_id' => null]);
        }
    }

    /**
     * POST /api/v1/mobile/campaigns/{id}/send
     *
     * Kicks the campaign into the send pipeline. Delegates to
     * CampaignService::sendCampaign which:
     *   - validates the channel-specific pre-flight (email account
     *     connected, SMS integration configured, etc.)
     *   - resolves the audience into Recipient rows
     *   - chunks delivery jobs onto the queue
     *
     * Returns the updated row so the Flutter UI can re-render the
     * status chip from 'draft' → 'sending' without a second fetch.
     */
    public function send(Request $request, int $id): JsonResponse
    {
        $workspaceId = $request->user()->active_workspace_id;

        $campaign = Campaign::where('workspace_id', $workspaceId)->find($id);
        if (!$campaign) {
            return response()->json(['message' => 'Campaign not found.'], 404);
        }

        if (!in_array($campaign->status, ['draft', 'scheduled'], true)) {
            return response()->json([
                'message' => "Campaign cannot be sent — current status: {$campaign->status}.",
            ], 422);
        }

        try {
            $this->campaignService->sendCampaign($campaign);
        } catch (\App\Services\Queue\QueueWorkerDownException $e) {
            // Specific "background worker is dead" case. 503 + a
            // structured body so the mobile app can show an actionable
            // error instead of "Something went wrong". The campaign
            // stayed as draft (the service threw BEFORE the status
            // flip), so the client can safely retry once the worker
            // is running again.
            return response()->json([
                'message'    => $e->getMessage(),
                'error_code' => 'queue_worker_down',
                'queue'      => $e->queueName,
                'retryable'  => true,
            ], 503);
        } catch (\InvalidArgumentException $e) {
            // CampaignService throws InvalidArgumentException for the
            // pre-flight failures customers can fix (missing subject,
            // no email account, empty audience). Surface verbatim.
            return response()->json(['message' => $e->getMessage()], 422);
        } catch (\Throwable $e) {
            \Illuminate\Support\Facades\Log::error('Mobile campaign send failed', [
                'campaign_id' => $id,
                'error'       => $e->getMessage(),
            ]);
            return response()->json([
                'message' => 'Could not start sending: ' . $e->getMessage(),
            ], 500);
        }

        $campaign->refresh();
        $campaign->load('createdBy:id,name', 'emailAccount:id,display_name,email,provider', 'attachments', 'abTestVariants');

        return response()->json([
            'message' => __('Campaign sending has started.'),
            'data'    => $this->shapeRow($campaign, full: true),
        ]);
    }

    /**
     * POST /api/v1/mobile/campaigns/{id}/test
     * Body: { to_email }
     *
     * Sends ONE test email to the address provided, bypassing the
     * audience + recipient logic. Useful for the Flutter "Send test"
     * button when the agent wants to QA a template before broadcast.
     */
    public function sendTest(Request $request, int $id): JsonResponse
    {
        $workspaceId = $request->user()->active_workspace_id;

        $campaign = Campaign::where('workspace_id', $workspaceId)->find($id);
        if (!$campaign) {
            return response()->json(['message' => 'Campaign not found.'], 404);
        }

        $data = Validator::make($request->all(), [
            'to_email' => ['required', 'email'],
        ])->validate();

        // Pick the campaign's configured account, or fall back to the
        // workspace's first connected mailbox.
        $account = $campaign->emailAccount
            ?: EmailAccount::where('workspace_id', $workspaceId)
                ->where('status', 'connected')
                ->first();

        if (!$account) {
            return response()->json([
                'message' => 'No connected email account available to send the test from.',
            ], 422);
        }

        try {
            $this->campaignService->sendTestEmail($campaign, $data['to_email'], $account);
        } catch (\Throwable $e) {
            return response()->json(['message' => $e->getMessage()], 422);
        }

        return response()->json([
            'message' => __('Test email sent to :email', ['email' => $data['to_email']]),
        ]);
    }

    /**
     * DELETE /api/v1/mobile/campaigns/{id}
     * Soft-delete. Refused while the campaign is actively sending —
     * otherwise the in-flight queue jobs would lose their parent row.
     */
    public function destroy(Request $request, int $id): JsonResponse
    {
        $workspaceId = $request->user()->active_workspace_id;

        $campaign = Campaign::where('workspace_id', $workspaceId)->find($id);
        if (!$campaign) {
            return response()->json(['message' => 'Campaign not found.'], 404);
        }

        if ($campaign->status === 'sending') {
            return response()->json([
                'message' => 'Cannot delete a campaign that is currently sending.',
            ], 422);
        }

        $campaign->delete();

        return response()->json(['message' => __('Campaign deleted.')]);
    }

    // ── helpers ──────────────────────────────────────────────────

    /**
     * Shape a Campaign row for the mobile UI.
     *
     * `$full` includes editor-facing fields (body_html/body_json/
     * audience_meta) used by the detail screen. The list screen
     * doesn't need them — they bloat the payload.
     */
    private function shapeRow(Campaign $c, bool $full = false): array
    {
        $delivered = (int) ($c->delivered_count ?? 0);
        $opened    = (int) ($c->opened_count ?? 0);
        $clicked   = (int) ($c->clicked_count ?? 0);
        $bounced   = (int) ($c->bounced_count ?? 0);
        $unsub     = (int) ($c->unsubscribed_count ?? 0);

        $row = [
            'id'              => $c->id,
            'uuid'            => $c->uuid,
            'name'            => $c->name,
            'type'            => $c->type,
            'channel'         => $c->channel ?: 'email',
            'status'          => $c->status,
            'subject'         => $c->subject,
            'preview_text'    => $c->preview_text,
            'from_number'     => $c->from_number,
            'audience_type'   => $c->audience_type,
            'audience_id'     => $c->audience_id,
            'scheduled_at'    => $c->scheduled_at?->toIso8601String(),
            'sent_at'         => $c->sent_at?->toIso8601String(),
            'completed_at'    => $c->completed_at?->toIso8601String(),
            'created_at'      => $c->created_at?->toIso8601String(),
            'created_by'      => $c->createdBy ? [
                'id'   => $c->createdBy->id,
                'name' => $c->createdBy->name,
            ] : null,
            'stats' => [
                'recipients'    => (int) ($c->recipients_count ?? 0),
                'sent'          => (int) ($c->sent_count ?? 0),
                'delivered'     => $delivered,
                'opened'        => $opened,
                'clicked'       => $clicked,
                'bounced'       => $bounced,
                'unsubscribed'  => $unsub,
                'open_rate'     => $delivered > 0 ? round(($opened / $delivered) * 100, 1) : 0.0,
                'click_rate'    => $delivered > 0 ? round(($clicked / $delivered) * 100, 1) : 0.0,
                'bounce_rate'   => $delivered > 0 ? round(($bounced / $delivered) * 100, 1) : 0.0,
            ],
        ];

        if ($full) {
            $row['body_html']     = $c->body_html;
            $row['body_text']     = $c->body_text;
            $row['body_json']     = is_array($c->body_json) ? $c->body_json : null;
            $row['audience_meta'] = is_array($c->audience_meta) ? $c->audience_meta : null;
            $row['email_account'] = $c->emailAccount ? [
                'id'           => $c->emailAccount->id,
                'name'         => $c->emailAccount->display_name ?: $c->emailAccount->email,
                'email'        => $c->emailAccount->email,
                'provider'     => $c->emailAccount->provider,
            ] : null;
            $row['emails_per_minute']    = (int) ($c->emails_per_minute ?? 0);
            $row['batch_size']           = (int) ($c->batch_size ?? 0);
            $row['batch_delay_seconds']  = (int) ($c->batch_delay_seconds ?? 0);

            // Attachments — the Flutter detail screen renders these
            // inline so the user sees what files will be sent without
            // a second request.
            $row['attachments'] = $c->attachments->map(fn ($a) => [
                'id'                => $a->id,
                'original_filename' => $a->original_filename,
                'mime_type'         => $a->mime_type,
                'size'              => (int) $a->size,
                'size_human'        => $a->size_for_humans,
                'is_inline'         => (bool) $a->is_inline,
                'is_image'          => $a->isImage(),
            ])->all();

            // AB variants — surfaced on detail so the composer can
            // render the split badge ("A 50% / B 50%") without hitting
            // /campaigns/{id}/variants separately.
            $row['variants'] = $c->abTestVariants->map(fn ($v) => [
                'id'         => $v->id,
                'variant'    => $v->variant,
                'subject'    => $v->subject,
                'body_html'  => $v->body_html,
                'percentage' => (int) $v->percentage,
                'is_winner'  => (bool) $v->is_winner,
            ])->all();
        }

        return $row;
    }

    private function emptyMeta(): array
    {
        return ['current_page' => 1, 'per_page' => 25, 'total' => 0, 'has_more' => false];
    }
}
