<?php

namespace App\Http\Controllers;

use App\Mail\Workspace\WorkspaceRoleChangedMail;
use App\Models\Tenant;
use App\Models\User;
use App\Notifications\WorkspaceRoleChanged;
use App\Services\AuditLogger;
use App\Services\Settings;
use App\Support\AppMode;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;

/**
 * Workspace membership management (Team Workspaces P3, SaaS-only). JSON-
 * returning action endpoints (desktop-portable, per CLAUDE.md §Core patterns)
 * + one Inertia page render (`members`). Authorization splits into the
 * `WorkspacePolicy` tiers (owner-or-admin vs owner-only); the active workspace
 * is always the `users.tenant_id` pointer's tenant.
 */
class WorkspaceController extends Controller
{
    /**
     * Members page (Inertia). PROP CONTRACT (the Members.tsx component depends
     * on this shape exactly):
     *   { workspace:{id,name},
     *     members:[{id,name,email,role}],
     *     invitations:[{id,email,role,expires_at}],
     *     seat:{limit,used},
     *     abilities:{is_owner,can_manage} }
     */
    public function members(Request $request): Response
    {
        $tenant = $this->activeTenant($request);
        $user = $request->user();

        $myRole = $user->roleIn($tenant);
        $canManage = in_array($myRole, ['owner', 'admin'], true);

        // The member ROSTER (names + emails) + pending invitations are
        // management data — withheld from editors/viewers entirely so a
        // non-manager can't enumerate teammates' contact info. Non-managers get
        // a minimal "your membership" view (their own role + Leave).
        $members = [];
        $invitations = [];

        if ($canManage) {
            $roleOrder = ['owner' => 0, 'admin' => 1, 'editor' => 2, 'viewer' => 3];
            $members = $tenant->members()
                ->orderBy('users.name')
                ->get()
                ->sortBy(fn (User $m) => $roleOrder[$m->getRelationValue('pivot')?->role] ?? 9)
                ->map(fn (User $m) => [
                    'id' => $m->id,
                    'name' => $m->name,
                    'email' => $m->email,
                    'role' => $m->getRelationValue('pivot')?->role,
                    // Per-member AI token cap (null = uncapped within the pool;
                    // 0 = no AI). Owner rows are always null (pool binds them).
                    'ai_tokens_monthly' => $m->getRelationValue('pivot')?->ai_tokens_monthly !== null
                        ? (int) $m->getRelationValue('pivot')->ai_tokens_monthly
                        : null,
                ])
                ->values()
                ->all();

            $invitations = $tenant->invitations()
                ->whereNull('accepted_at')
                ->where(function ($q) {
                    $q->whereNull('expires_at')->orWhere('expires_at', '>', now());
                })
                ->orderByDesc('id')
                ->get()
                ->map(fn ($inv) => [
                    'id' => $inv->id,
                    'email' => $inv->email,
                    'role' => $inv->role,
                    'ai_tokens_monthly' => $inv->ai_tokens_monthly !== null
                        ? (int) $inv->ai_tokens_monthly
                        : null,
                    'expires_at' => $inv->expires_at?->toIso8601String(),
                ])
                ->values()
                ->all();
        }

        return Inertia::render('Workspace/Members', [
            'workspace' => [
                'id' => $tenant->id,
                'name' => $tenant->name,
            ],
            'members' => $members,
            'invitations' => $invitations,
            // Seat usage is owner/admin-only management info — withheld from
            // editors/viewers entirely (not just hidden in the UI).
            'seat' => $canManage ? [
                'limit' => $tenant->seatLimit(),
                'used' => $tenant->seatUsage(),
            ] : null,
            // The current user's own role — drives the non-manager self-view.
            'myRole' => $myRole,
            'abilities' => [
                'is_owner' => $user->isOwnerOf($tenant),
                'can_manage' => $canManage,
            ],
            // AI allocation controls only surface when the workspace can
            // actually spend tokens: global AI on + the workspace (owner's)
            // plan grants `can_use_ai`. Self-hosted has no per-member caps
            // (single unlimited pool), so it's off there. `pool_limit` = the
            // shared monthly cap (null = unlimited) shown as the allocation
            // ceiling hint.
            'ai' => $canManage ? [
                'enabled' => ! AppMode::isSelfHosted()
                    && (bool) Settings::get('ai.enabled')
                    && (bool) $tenant->activePlan()->feature('can_use_ai', false),
                'pool_limit' => $tenant->activePlan()->aiTokensMonthly(),
            ] : null,
        ]);
    }

    /**
     * Per-workspace activity log (Inertia) — the tenant-facing "who did what"
     * trail (invites, role changes, member removals, project shares, rename).
     * Reads the SAME `audit_logs` rows as the operator's admin log but scoped to
     * THIS tenant via `context->tenant_id`, so an owner/admin sees only their
     * own workspace's history. Owner/admin only (management surface).
     *
     * PROP CONTRACT (Workspace/Activity.tsx depends on this):
     *   { workspace:{id,name},
     *     events: paginated{ id, action, summary, actor, created_at },
     *     actors: [{id,name}], filters:{type,actor} }
     */
    public function activity(Request $request): Response
    {
        $tenant = $this->activeTenant($request);
        $user = $request->user();

        // Management surface — editors/viewers can't see the audit trail.
        abort_unless(in_array($user->roleIn($tenant), ['owner', 'admin'], true), 403);

        // Type filter → action set. `invites` is a prefix; the rest are exact
        // sets. An unknown/absent type means "all".
        $typeActions = [
            'members' => ['workspace.member.removed', 'workspace.member.left', 'workspace.role.changed', 'workspace.ai_allocation.changed'],
            'projects' => ['workspace.project.created', 'workspace.project.deleted', 'workspace.project.shared', 'workspace.project.unshared'],
            'settings' => ['workspace.renamed'],
        ];
        $type = (string) $request->query('type', 'all');
        $actorId = $request->query('actor');
        $actorId = is_numeric($actorId) ? (int) $actorId : null;

        // Members once → both the actor-role map (tags each event's actor) and
        // the actor filter options.
        $members = $tenant->members()->orderBy('users.name')->get();
        $roleByUserId = $members
            ->mapWithKeys(fn (User $m) => [$m->id => $m->getRelationValue('pivot')?->role])
            ->all();

        $query = \App\Models\AuditLog::query()
            ->where('action', 'like', 'workspace.%')
            ->where('action', '!=', 'workspace.switch') // legacy noise (no longer recorded)
            ->where('context->tenant_id', $tenant->id);

        if ($type === 'invites') {
            $query->where('action', 'like', 'workspace.invite.%');
        } elseif (isset($typeActions[$type])) {
            $query->whereIn('action', $typeActions[$type]);
        } else {
            $type = 'all';
        }

        if ($actorId !== null) {
            $query->where('actor_user_id', $actorId);
        }

        $events = $query
            ->with('actor:id,name,email')
            ->latest()
            ->paginate(30)
            ->withQueryString() // preserve type/actor across page links
            ->through(fn (\App\Models\AuditLog $log) => [
                'id' => $log->id,
                'action' => $log->action,
                'summary' => $log->summary,
                // Structured: role tags the actor (owner → "Owner"), email
                // disambiguates same-named members (names aren't unique).
                'actor' => $log->actor ? [
                    'name' => $log->actor->name,
                    'email' => $log->actor->email,
                    'role' => $roleByUserId[$log->actor_user_id] ?? null,
                ] : null,
                'created_at' => $log->created_at?->toIso8601String(),
            ]);

        // Actor options for the filter — the workspace's members (managers see
        // the roster anyway). Email rides along for same-name disambiguation.
        $actors = $members
            ->map(fn (User $m) => ['id' => $m->id, 'name' => $m->name, 'email' => $m->email])
            ->values();

        return Inertia::render('Workspace/Activity', [
            'workspace' => [
                'id' => $tenant->id,
                'name' => $tenant->name,
            ],
            'events' => $events,
            'actors' => $actors,
            'filters' => [
                'type' => $type,
                'actor' => $actorId,
            ],
        ]);
    }

    /**
     * Rename the active workspace (owner-or-admin).
     */
    public function rename(Request $request): RedirectResponse
    {
        $tenant = $this->activeTenant($request);
        $this->authorizeWorkspace($request, 'rename', $tenant);

        $data = $request->validate([
            'name' => ['required', 'string', 'max:120'],
        ]);

        $old = $tenant->name;
        $tenant->forceFill(['name' => $data['name']])->save();

        AuditLogger::record(
            'workspace.renamed',
            $tenant,
            "Renamed workspace from \"{$old}\" to \"{$tenant->name}\"",
            ['tenant_id' => $tenant->id],
        );

        return back();
    }

    /**
     * Remove a member from the active workspace. owner-or-admin; an admin
     * can't remove the owner or another admin (owner-only mints/removes
     * admins). The removed member's active pointer flips to their personal
     * workspace; their project grants cascade.
     */
    public function removeMember(Request $request, User $user): RedirectResponse
    {
        $member = $user;
        $tenant = $this->activeTenant($request);

        $targetRole = $member->roleIn($tenant);
        if ($targetRole === null) {
            abort(404);
        }

        if ($targetRole === 'owner') {
            abort(403, 'The owner cannot be removed.');
        }

        // owner-or-admin gate; admins additionally can't remove another admin.
        if ($targetRole === 'admin') {
            $this->authorizeWorkspace($request, 'removeAdmin', $tenant);
        } else {
            $this->authorizeWorkspace($request, 'removeEditorOrViewer', $tenant);
        }

        DB::transaction(function () use ($tenant, $member) {
            $tenant->members()->detach($member->id);
            $this->repointIfActive($member, $tenant);
        });

        AuditLogger::record(
            'workspace.member.removed',
            $tenant,
            "Removed {$member->email} from the workspace",
            ['tenant_id' => $tenant->id, 'member_id' => $member->id, 'role' => $targetRole],
        );

        return back();
    }

    /**
     * Change a member's role. editor↔viewer by owner-or-admin; set/remove the
     * Admin role is owner-only (Δ24). A viewer→editor/admin promotion consumes
     * a seat → row-locked re-check (Δ17).
     */
    public function changeRole(Request $request, User $user): JsonResponse
    {
        $member = $user;
        $tenant = $this->activeTenant($request);

        $data = $request->validate([
            'role' => ['required', Rule::in(['admin', 'editor', 'viewer'])],
        ]);
        $newRole = $data['role'];

        $current = $member->roleIn($tenant);
        if ($current === null) {
            abort(404);
        }
        if ($current === 'owner') {
            abort(403, 'The owner role is changed via transfer, not role edits.');
        }

        $touchesAdmin = $newRole === 'admin' || $current === 'admin';
        if ($touchesAdmin) {
            // Mint or remove an Admin — owner-only.
            $this->authorizeWorkspace($request, 'setAdminRole', $tenant);
        } else {
            $this->authorizeWorkspace($request, 'changeEditorViewerRole', $tenant);
        }

        if ($newRole === $current) {
            return response()->json(['ok' => true, 'role' => $newRole]);
        }

        // viewer → edit-capable (editor|admin) consumes a seat (Δ17/Δ22).
        $promotesToSeat = $current === 'viewer' && in_array($newRole, ['editor', 'admin'], true);

        DB::transaction(function () use ($tenant, $member, $newRole, $promotesToSeat) {
            if ($promotesToSeat) {
                $locked = Tenant::query()->whereKey($tenant->id)->lockForUpdate()->first();
                $limit = $locked->seatLimit();
                if ($limit !== null && ($locked->seatUsage() + $locked->pendingEditorInviteCount()) >= $limit) {
                    throw ValidationException::withMessages([
                        'role' => ['Seat limit reached. Upgrade the plan or remove a member to promote.'],
                    ]);
                }
            }

            $pivot = ['role' => $newRole];
            // Demoting to Viewer clears any AI allocation — viewers can't spend
            // AI, and a stale cap shouldn't silently resurface on re-promotion.
            if ($newRole === 'viewer') {
                $pivot['ai_tokens_monthly'] = null;
            }
            $tenant->members()->updateExistingPivot($member->id, $pivot);
        });

        AuditLogger::record(
            'workspace.role.changed',
            $tenant,
            "Changed {$member->email} role from {$current} to {$newRole}",
            ['tenant_id' => $tenant->id, 'member_id' => $member->id, 'from' => $current, 'to' => $newRole],
        );

        // Tell the member their access changed — in-app bell row (synchronous)
        // + queued email, always-on.
        $actorName = $request->user()->name ?: $request->user()->email;
        $member->notify(new WorkspaceRoleChanged($tenant->name, $newRole, $actorName));
        Mail::to($member->email)->queue(new WorkspaceRoleChangedMail(
            recipientName: (string) $member->name,
            workspaceName: $tenant->name,
            role: $newRole,
            actorName: $actorName,
        ));

        return response()->json(['ok' => true, 'role' => $newRole]);
    }

    /**
     * Set a member's per-workspace AI token allocation (owner/admin). Writes the
     * `tenant_user.ai_tokens_monthly` pivot column read by `User::aiMemberLimit()`
     * — null = uncapped within the shared pool, 0 = no AI, N = monthly cap. The
     * owner has no per-member cap (they're the billing anchor and draw on the
     * full pool), so their row is refused.
     */
    public function setAiAllocation(Request $request, User $user): JsonResponse
    {
        $member = $user;
        $tenant = $this->activeTenant($request);

        // Same management gate as editor/viewer role edits (owner-or-admin).
        $this->authorizeWorkspace($request, 'changeEditorViewerRole', $tenant);

        $current = $member->roleIn($tenant);
        if ($current === null) {
            abort(404);
        }
        if ($current === 'owner') {
            abort(403, 'The owner draws on the full workspace pool — no per-member cap applies.');
        }

        $data = $request->validate([
            'ai_tokens_monthly' => ['present', 'nullable', 'integer', 'min:0', 'max:1000000000'],
        ]);
        $allocation = $data['ai_tokens_monthly'];

        $tenant->members()->updateExistingPivot($member->id, [
            'ai_tokens_monthly' => $allocation,
        ]);

        AuditLogger::record(
            'workspace.ai_allocation.changed',
            $tenant,
            $allocation === null
                ? "Set {$member->email} AI allocation to the shared pool (uncapped)"
                : "Set {$member->email} AI allocation to {$allocation} tokens/mo",
            [
                'tenant_id' => $tenant->id,
                'member_id' => $member->id,
                'ai_tokens_monthly' => $allocation,
            ],
        );

        return response()->json(['ok' => true, 'ai_tokens_monthly' => $allocation]);
    }

    /**
     * Leave a workspace you were invited to (any non-owner). The owner of a
     * workspace can't leave it (it's their own). Active pointer flips to the
     * personal workspace.
     */
    public function leave(Request $request): RedirectResponse
    {
        $tenant = $this->activeTenant($request);
        $user = $request->user();

        $role = $user->roleIn($tenant);
        if ($role === null) {
            abort(404);
        }
        if ($role === 'owner') {
            abort(403, 'You own this workspace, so you cannot leave it.');
        }
        if ($tenant->personal) {
            abort(403, 'You cannot leave your personal workspace.');
        }

        DB::transaction(function () use ($tenant, $user) {
            $tenant->members()->detach($user->id);
            $this->repointIfActive($user, $tenant);
        });

        AuditLogger::record(
            'workspace.member.left',
            $tenant,
            "{$user->email} left the workspace",
            ['tenant_id' => $tenant->id, 'member_id' => $user->id, 'role' => $role],
        );

        return redirect('/projects')->with('success', 'You left the workspace.');
    }

    /**
     * Switch the active workspace (any member). Validate pivot membership →
     * flip `users.tenant_id` → audit → full Inertia visit to /projects.
     */
    public function switch(Request $request): RedirectResponse
    {
        $user = $request->user();

        $data = $request->validate([
            'tenant' => ['required', 'integer'],
        ]);

        $target = Tenant::query()->find($data['tenant']);
        if ($target === null || $user->roleIn($target) === null) {
            abort(403, 'You are not a member of that workspace.');
        }

        // Switching the active workspace is navigation, not a state change —
        // deliberately NOT audited (it added pure noise to the operator log + a
        // workspace's own activity feed). The pointer flip below is all there
        // is to it.
        if ($user->tenant_id !== $target->id) {
            $user->forceFill(['tenant_id' => $target->id])->save();
        }

        return redirect('/projects');
    }

    // -- helpers -----------------------------------------------------------

    private function activeTenant(Request $request): Tenant
    {
        $user = $request->user();
        $tenant = $user?->tenant;
        if ($tenant === null) {
            abort(404);
        }

        return $tenant;
    }

    private function authorizeWorkspace(Request $request, string $ability, Tenant $tenant): void
    {
        if (! $request->user()->can($ability, $tenant)) {
            abort(403);
        }
    }

    /**
     * If `$member`'s active pointer targets `$tenant`, flip it to their
     * personal workspace (they just left / were removed from `$tenant`).
     */
    private function repointIfActive(User $member, Tenant $tenant): void
    {
        if ($member->tenant_id === $tenant->id) {
            $personal = $member->personalWorkspace();
            $member->forceFill(['tenant_id' => $personal?->id])->save();
        }
    }
}
