<?php

namespace App\Services\AccessControl;

use App\Models\User;
use App\Models\Victim;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Validation\ValidationException;

class DataScopeService
{
    public function userHasDataScope(User $user, string $scope): bool
    {
        if (! method_exists($user, 'hasNgbvdPermission')) {
            return false;
        }

        return $user->hasNgbvdPermission('data_scope.' . $scope);
    }

    public function userHasNationalAccess(User $user): bool
    {
        return $this->userHasDataScope($user, 'national');
    }

    public function userHasDistrictAccess(User $user): bool
    {
        return $this->userHasDataScope($user, 'district');
    }

    public function userHasPartnerAccess(User $user): bool
    {
        return $this->userHasDataScope($user, 'partner');
    }

    public function userHasOwnRecordsAccess(User $user): bool
    {
        return $this->userHasDataScope($user, 'own_records');
    }

    public function districtIdsFor(User $user): array
    {
        if ($this->userHasNationalAccess($user) || ! $this->userHasDistrictAccess($user)) {
            return [];
        }

        $ids = $this->parseCsvIds($user->district);

        if (Schema::hasTable('user_data_scopes')) {
            $extra = DB::table('user_data_scopes')
                ->where('user_id', $user->id)
                ->where('scope_type', 'district')
                ->where('is_active', 1)
                ->pluck('scope_value')
                ->all();
            $ids = array_merge($ids, $this->parseCsvIds(implode(',', $extra)));
        }

        return array_values(array_unique(array_filter($ids, function ($id) {
            return $id !== '';
        })));
    }

    public function partnerIdsFor(User $user): array
    {
        if ($this->userHasNationalAccess($user) || ! $this->userHasPartnerAccess($user)) {
            return [];
        }

        $ids = [];
        if (!empty($user->partnerid)) {
            $ids[] = (string) $user->partnerid;
        }
        if (!empty($user->partner) && (string) $user->partner !== '1') {
            $ids[] = (string) $user->partner;
        }

        if (Schema::hasTable('user_data_scopes')) {
            $extra = DB::table('user_data_scopes')
                ->where('user_id', $user->id)
                ->where('scope_type', 'partner')
                ->where('is_active', 1)
                ->pluck('scope_value')
                ->all();
            $ids = array_merge($ids, $this->parseCsvIds(implode(',', $extra)));
        }

        return array_values(array_unique(array_filter($ids, function ($id) {
            return $id !== '';
        })));
    }

    public function scopeLabelFor(User $user): string
    {
        if ($this->userHasNationalAccess($user)) {
            return 'National';
        }

        $parts = [];

        if ($this->userHasDistrictAccess($user)) {
            $count = count($this->districtIdsFor($user));
            $parts[] = $count > 1 ? $count . ' Districts' : 'District';
        }

        if ($this->userHasPartnerAccess($user)) {
            $parts[] = 'Partner';
        }

        if ($this->userHasOwnRecordsAccess($user)) {
            $parts[] = 'Own Records';
        }

        return empty($parts) ? 'No data scope assigned' : implode(' + ', $parts);
    }

    public function cacheSignature(User $user): string
    {
        return md5(json_encode([
            'user_id' => $user->id,
            'role' => $user->userrole,
            'district' => $user->district,
            'partner' => $user->partner,
            'partnerid' => $user->partnerid,
            'national' => $this->userHasNationalAccess($user),
            'district_scope' => $this->userHasDistrictAccess($user),
            'districts' => $this->districtIdsFor($user),
            'partner_scope' => $this->userHasPartnerAccess($user),
            'partners' => $this->partnerIdsFor($user),
            'own' => $this->userHasOwnRecordsAccess($user),
        ]));
    }

    public function applyToUserQuery($query, User $actor, string $usersAlias = 'users')
    {
        if ($this->userHasNationalAccess($actor)) {
            return $query;
        }

        $districtIds = $this->districtIdsFor($actor);
        $partnerIds = $this->partnerIdsFor($actor);

        return $query->where(function ($q) use ($usersAlias, $districtIds, $partnerIds, $actor) {
            $applied = false;

            if (!empty($districtIds)) {
                foreach ($districtIds as $districtId) {
                    $method = $applied ? 'orWhereRaw' : 'whereRaw';
                    $q->{$method}("FIND_IN_SET(?, REPLACE({$usersAlias}.district, ' ', ''))", [$districtId]);
                    $applied = true;
                }
            }

            if (!empty($partnerIds)) {
                $method = $applied ? 'orWhereIn' : 'whereIn';
                $q->{$method}($usersAlias . '.partnerid', $partnerIds);
                $applied = true;
            }

            if ($this->userHasOwnRecordsAccess($actor)) {
                $method = $applied ? 'orWhere' : 'where';
                $q->{$method}($usersAlias . '.created_by', $actor->id);
                $applied = true;
            }

            if (!$applied) {
                $q->whereRaw('1 = 0');
            }
        });
    }

    public function applyToVictimTableQuery($query, User $user, string $victimAlias = 'victim')
    {
        if ($this->userHasNationalAccess($user)) {
            return $query;
        }

        $districtIds = $this->districtIdsFor($user);
        $partnerIds = $this->partnerIdsFor($user);

        return $query->where(function ($q) use ($victimAlias, $districtIds, $partnerIds, $user) {
            $applied = false;

            if (!empty($districtIds)) {
                $method = $applied ? 'orWhereIn' : 'whereIn';
                $q->{$method}($victimAlias . '.districtid', $districtIds);
                $applied = true;
            }

            if (!empty($partnerIds)) {
                $method = $applied ? 'orWhereExists' : 'whereExists';
                $q->{$method}(function ($sub) use ($victimAlias, $partnerIds) {
                    $sub->select(DB::raw(1))
                        ->from('users as scope_users')
                        ->whereColumn('scope_users.id', $victimAlias . '.created_by')
                        ->where(function ($partnerQuery) use ($partnerIds) {
                            $partnerQuery->whereIn('scope_users.partnerid', $partnerIds)
                                ->orWhereIn('scope_users.partner', $partnerIds);
                        });
                });
                $applied = true;
            }

            if ($this->userHasOwnRecordsAccess($user)) {
                $method = $applied ? 'orWhere' : 'where';
                $q->{$method}($victimAlias . '.created_by', $user->id);
                $applied = true;
            }

            if (!$applied) {
                $q->whereRaw('1 = 0');
            }
        });
    }

    public function applyToIncidentDataQuery($query, User $user, string $incidentAlias = 'incident_data', ?string $victimAlias = null)
    {
        if ($this->userHasNationalAccess($user)) {
            return $query;
        }

        $districtIds = $this->districtIdsFor($user);
        $partnerIds = $this->partnerIdsFor($user);

        return $query->where(function ($q) use ($incidentAlias, $victimAlias, $districtIds, $partnerIds, $user) {
            $applied = false;

            if (!empty($districtIds)) {
                $method = $applied ? 'orWhereIn' : 'whereIn';
                $q->{$method}($incidentAlias . '.districtid', $districtIds);
                $applied = true;
            }

            if (!empty($partnerIds)) {
                $method = $applied ? 'orWhere' : 'where';
                $q->{$method}(function ($partnerScope) use ($incidentAlias, $victimAlias, $partnerIds) {
                    $partnerScope->whereExists(function ($sub) use ($incidentAlias, $partnerIds) {
                        $sub->select(DB::raw(1))
                            ->from('users as scope_incident_users')
                            ->whereColumn('scope_incident_users.id', $incidentAlias . '.created_by')
                            ->where(function ($partnerQuery) use ($partnerIds) {
                                $partnerQuery->whereIn('scope_incident_users.partnerid', $partnerIds)
                                    ->orWhereIn('scope_incident_users.partner', $partnerIds);
                            });
                    });

                    if ($victimAlias) {
                        $partnerScope->orWhereExists(function ($sub) use ($victimAlias, $partnerIds) {
                            $sub->select(DB::raw(1))
                                ->from('users as scope_victim_users')
                                ->whereColumn('scope_victim_users.id', $victimAlias . '.created_by')
                                ->where(function ($partnerQuery) use ($partnerIds) {
                                    $partnerQuery->whereIn('scope_victim_users.partnerid', $partnerIds)
                                        ->orWhereIn('scope_victim_users.partner', $partnerIds);
                                });
                        });
                    } else {
                        $partnerScope->orWhereExists(function ($sub) use ($incidentAlias, $partnerIds) {
                            $sub->select(DB::raw(1))
                                ->from('victim as scope_victim_partner')
                                ->join('users as scope_victim_users', 'scope_victim_partner.created_by', '=', 'scope_victim_users.id')
                                ->whereColumn('scope_victim_partner.id', $incidentAlias . '.victimid')
                                ->where(function ($partnerQuery) use ($partnerIds) {
                                    $partnerQuery->whereIn('scope_victim_users.partnerid', $partnerIds)
                                        ->orWhereIn('scope_victim_users.partner', $partnerIds);
                                });
                        });
                    }
                });
                $applied = true;
            }

            if ($this->userHasOwnRecordsAccess($user)) {
                $method = $applied ? 'orWhere' : 'where';
                $q->{$method}(function ($ownScope) use ($incidentAlias, $victimAlias, $user) {
                    $ownScope->where($incidentAlias . '.created_by', $user->id);

                    if ($victimAlias) {
                        $ownScope->orWhere($victimAlias . '.created_by', $user->id);
                    } else {
                        $ownScope->orWhereExists(function ($sub) use ($incidentAlias, $user) {
                            $sub->select(DB::raw(1))
                                ->from('victim as scope_victim_own')
                                ->whereColumn('scope_victim_own.id', $incidentAlias . '.victimid')
                                ->where('scope_victim_own.created_by', $user->id);
                        });
                    }
                });
                $applied = true;
            }

            if (!$applied) {
                $q->whereRaw('1 = 0');
            }
        });
    }

    public function userCanAccessVictimReference(User $user, $reference): bool
    {
        if ($reference instanceof Victim) {
            return $this->userCanAccessVictim($user, $reference);
        }

        if (is_object($reference) && isset($reference->id)) {
            $reference = $reference->id;
        }

        if ($this->userHasNationalAccess($user)) {
            return true;
        }

        if (Schema::hasColumn((new Victim())->getTable(), 'uuid') && is_string($reference) && preg_match('/^[0-9a-fA-F-]{36}$/', $reference)) {
            $victim = Victim::where('uuid', $reference)->first();
        } else {
            $victimId = $this->decodeReference($reference);
            if (!$victimId) {
                return false;
            }

            $victim = Victim::find($victimId);
        }
        if (!$victim) {
            return false;
        }

        return $this->userCanAccessVictim($user, $victim);
    }

    public function userCanAccessVictim(User $user, Victim $victim): bool
    {
        if ($this->userHasNationalAccess($user)) {
            return true;
        }

        $victimId = $victim->id;
        $districtIds = array_map('strval', $this->districtIdsFor($user));
        $partnerIds = array_map('strval', $this->partnerIdsFor($user));

        // District scope: allow access when either the survivor profile or any linked
        // incident record belongs to one of the user's assigned districts. This supports
        // legacy records where district data may be more reliable on incident_data.
        if (!empty($districtIds)) {
            if (!empty($victim->districtid) && in_array((string) $victim->districtid, $districtIds, true)) {
                return true;
            }

            if (Schema::hasTable('incident_data')) {
                $incidentDistrictMatch = DB::table('incident_data')
                    ->where('victimid', $victimId)
                    ->whereIn('districtid', $districtIds)
                    ->exists();

                if ($incidentDistrictMatch) {
                    return true;
                }
            }
        }

        // Partner scope: allow access where the case creator or linked incident creator
        // belongs to the user's assigned partner. A user can only belong to one partner,
        // but the method supports an array for compatibility with user_data_scopes.
        if (!empty($partnerIds)) {
            if (!empty($victim->created_by) && $this->userIdBelongsToPartner($victim->created_by, $partnerIds)) {
                return true;
            }

            if (Schema::hasTable('incident_data')) {
                $incidentPartnerMatch = DB::table('incident_data as scoped_incidents')
                    ->join('users as scoped_users', 'scoped_incidents.created_by', '=', 'scoped_users.id')
                    ->where('scoped_incidents.victimid', $victimId)
                    ->where(function ($query) use ($partnerIds) {
                        $query->whereIn('scoped_users.partnerid', $partnerIds)
                            ->orWhereIn('scoped_users.partner', $partnerIds);
                    })
                    ->exists();

                if ($incidentPartnerMatch) {
                    return true;
                }
            }
        }

        // Own-record scope: allow access when the user created/updated the case or any
        // linked workflow section. This prevents blocking a user during the same case
        // capture workflow while still denying unrelated records.
        if ($this->userHasOwnRecordsAccess($user)) {
            if ((string) ($victim->created_by ?? '') === (string) $user->id ||
                (string) ($victim->updated_by ?? '') === (string) $user->id) {
                return true;
            }

            if ($this->userHasLinkedCaseActivity($user, $victimId)) {
                return true;
            }
        }

        return false;
    }


    public function assertCanWriteCaseSection(User $user, Victim $victim, $districtId = null, $partnerId = null): void
    {
        $this->assertCanAccessVictim($user, $victim);

        if (!empty($districtId)) {
            $this->assertDistrictAllowed($user, $districtId);
        }

        if (!empty($partnerId)) {
            $this->assertPartnerAllowed($user, $partnerId);
        }
    }

    private function userIdBelongsToPartner($userId, array $partnerIds): bool
    {
        if (empty($userId) || empty($partnerIds) || !Schema::hasTable('users')) {
            return false;
        }

        return DB::table('users')
            ->where('id', $userId)
            ->where(function ($query) use ($partnerIds) {
                $query->whereIn('partnerid', $partnerIds)
                    ->orWhereIn('partner', $partnerIds);
            })
            ->exists();
    }

    private function userHasLinkedCaseActivity(User $user, $victimId): bool
    {
        $tables = [
            ['table' => 'incident_data', 'victim_column' => 'victimid'],
            ['table' => 'perpetrator', 'victim_column' => 'victimid'],
            ['table' => 'witness', 'victim_column' => 'victim_id'],
            ['table' => 'action', 'victim_column' => 'victim_id'],
            ['table' => 'incident_status', 'victim_column' => 'victimid'],
        ];

        foreach ($tables as $meta) {
            if (!Schema::hasTable($meta['table']) || !Schema::hasColumn($meta['table'], $meta['victim_column'])) {
                continue;
            }

            $query = DB::table($meta['table'])->where($meta['victim_column'], $victimId);

            $query->where(function ($activityQuery) use ($meta, $user) {
                $hasCreatedBy = Schema::hasColumn($meta['table'], 'created_by');
                $hasUpdatedBy = Schema::hasColumn($meta['table'], 'updated_by');

                if ($hasCreatedBy) {
                    $activityQuery->where('created_by', $user->id);
                }

                if ($hasUpdatedBy) {
                    $hasCreatedBy ? $activityQuery->orWhere('updated_by', $user->id) : $activityQuery->where('updated_by', $user->id);
                }

                if (!$hasCreatedBy && !$hasUpdatedBy) {
                    $activityQuery->whereRaw('1 = 0');
                }
            });

            if ($query->exists()) {
                return true;
            }
        }

        return false;
    }

    public function assertDistrictAllowed(User $user, $districtId): void
    {
        if ($this->userHasNationalAccess($user) || empty($districtId)) {
            return;
        }

        $allowedDistrictIds = $this->districtIdsFor($user);

        if (empty($allowedDistrictIds) || !in_array((string) $districtId, array_map('strval', $allowedDistrictIds), true)) {
            throw ValidationException::withMessages([
                'districtid' => 'You are not allowed to create or update records for the selected District.',
            ]);
        }
    }

    public function assertPartnerAllowed(User $user, $partnerId): void
    {
        if ($this->userHasNationalAccess($user) || empty($partnerId)) {
            return;
        }

        $allowedPartnerIds = $this->partnerIdsFor($user);

        if (empty($allowedPartnerIds) || !in_array((string) $partnerId, array_map('strval', $allowedPartnerIds), true)) {
            throw ValidationException::withMessages([
                'partnerid' => 'You are not allowed to create or update records for the selected Partner Organisation.',
            ]);
        }
    }

    public function assertCanAccessVictim(User $user, $victimReference): void
    {
        if (!$this->userCanAccessVictimReference($user, $victimReference)) {
            throw ValidationException::withMessages([
                'record' => 'You are not allowed to access this case record based on your assigned data scope.',
            ]);
        }
    }

    public function assertCanAssignDistricts(User $actor, array $districtIds): void
    {
        if ($this->userHasNationalAccess($actor)) {
            return;
        }

        $allowed = $this->districtIdsFor($actor);

        foreach ($districtIds as $districtId) {
            if ($districtId !== '' && !in_array((string) $districtId, array_map('strval', $allowed), true)) {
                throw ValidationException::withMessages([
                    'district' => 'You can only assign users to districts within your permitted data scope.',
                ]);
            }
        }
    }

    public function assertCanAssignPartner(User $actor, $partnerId): void
    {
        if ($this->userHasNationalAccess($actor) || empty($partnerId)) {
            return;
        }

        $allowed = $this->partnerIdsFor($actor);
        if (!in_array((string) $partnerId, array_map('strval', $allowed), true)) {
            throw ValidationException::withMessages([
                'partnerid' => 'You can only assign users to partner organisations within your permitted data scope.',
            ]);
        }
    }

    private function decodeReference($reference)
    {
        if (empty($reference)) {
            return null;
        }

        if (is_numeric($reference)) {
            return (int) $reference;
        }

        try {
            $decoded = Crypt::decryptString($reference);
            return is_numeric($decoded) ? (int) $decoded : $decoded;
        } catch (DecryptException $e) {
            $decoded = base64_decode($reference, true);
            return is_numeric($decoded) ? (int) $decoded : null;
        } catch (\Throwable $e) {
            return null;
        }
    }

    private function parseCsvIds($value): array
    {
        if ($value === null || $value === '' || $value === '*') {
            return [];
        }

        if (is_array($value)) {
            return array_values(array_filter(array_map('strval', $value), function ($item) {
                return $item !== '' && $item !== '*';
            }));
        }

        return array_values(array_filter(array_map(function ($item) {
            return trim((string) $item);
        }, explode(',', (string) $value)), function ($item) {
            return $item !== '' && $item !== '*';
        }));
    }
}
