<?php

namespace App\Services\Search;

use App\Models\CandidateProfileModel;
use App\Models\CandidateTagModel;
use App\Models\CandidateUnlockModel;
use App\Models\SavedCandidateModel;
use App\Models\ShortlistModel;
use App\Services\Billing\CreditService;
use App\Services\Support\ActivityLogger;
use Config\Database;

/**
 * Candidate database search + talent-pool actions. Search itself is
 * deliberately NOT run through BaseRepository tenant scoping — candidates
 * are a platform-global pool, not agency-owned data. What IS tenant-scoped
 * is which candidates a given agency has *unlocked* (candidate_unlocks),
 * which controls what the redacted search result exposes.
 */
class CandidateSearchService
{
    private CandidateProfileModel $profiles;
    private CandidateUnlockModel $unlocks;
    private SavedCandidateModel $saved;
    private ShortlistModel $shortlists;
    private CandidateTagModel $tags;
    private CreditService $credits;
    private ActivityLogger $activity;

    public function __construct()
    {
        $this->profiles   = new CandidateProfileModel();
        $this->unlocks    = new CandidateUnlockModel();
        $this->saved      = new SavedCandidateModel();
        $this->shortlists = new ShortlistModel();
        $this->tags       = new CandidateTagModel();
        $this->credits    = new CreditService();
        $this->activity   = new ActivityLogger();
    }

    /**
     * Filters: q (keyword against headline/about), skills[], location,
     * availability_status, work_preference, employment_type, career_level,
     * industry, salary_min, salary_max, is_verified, min_profile_pct,
     * last_active_days.
     */
    public function search(array $filters, int $agencyId, int $page = 1, int $perPage = 20): array
    {
        $builder = Database::connect()->table('candidate_profiles cp')
            ->select('cp.*, u.name, u.email, u.phone, u.profile_photo_path')
            ->join('users u', 'u.id = cp.user_id')
            ->where('cp.is_public', 1);

        if (! empty($filters['q'])) {
            $builder->groupStart()
                ->like('cp.headline', $filters['q'])
                ->orLike('cp.about', $filters['q'])
                ->orLike('u.name', $filters['q'])
            ->groupEnd();
        }
        if (! empty($filters['availability_status'])) {
            $builder->where('cp.availability_status', $filters['availability_status']);
        }
        if (! empty($filters['work_preference'])) {
            $builder->where('cp.work_preference', $filters['work_preference']);
        }
        if (! empty($filters['employment_type'])) {
            $builder->groupStart()
                ->where('cp.preferred_employment_type', $filters['employment_type'])
                ->orWhere('cp.preferred_employment_type', 'any')
            ->groupEnd();
        }
        if (! empty($filters['career_level'])) {
            $builder->where('cp.career_level', $filters['career_level']);
        }
        if (! empty($filters['industry'])) {
            $builder->like('cp.industry_preferences', $filters['industry']);
        }
        if (! empty($filters['location'])) {
            $builder->like('cp.preferred_locations', $filters['location']);
        }
        if (! empty($filters['salary_max'])) {
            $builder->where('cp.salary_expectation_min <=', $filters['salary_max']);
        }
        if (! empty($filters['is_verified'])) {
            $builder->where('cp.is_verified', 1);
        }
        if (! empty($filters['min_profile_pct'])) {
            $builder->where('cp.profile_completion_pct >=', $filters['min_profile_pct']);
        }
        if (! empty($filters['last_active_days'])) {
            $builder->where('cp.last_active_at >=', date('Y-m-d', strtotime("-{$filters['last_active_days']} days")));
        }
        if (! empty($filters['skills'])) {
            $skills = is_array($filters['skills']) ? $filters['skills'] : array_map('trim', explode(',', $filters['skills']));
            $builder->join('candidate_skills cs', 'cs.candidate_profile_id = cp.id')
                ->join('skills s', 's.id = cs.skill_id')
                ->whereIn('s.name', $skills)
                ->groupBy('cp.id');
        }

        $offset = ($page - 1) * $perPage;
        $rows = $builder->orderBy('cp.profile_completion_pct', 'DESC')->limit($perPage, $offset)->get()->getResultArray();

        // Redact contact info for candidates this agency hasn't unlocked —
        // enforced here at the query-result layer, not left to the frontend
        // (see 09-security-plan.md §5).
        foreach ($rows as &$row) {
            $unlocked = $this->unlocks->anyUnlockFor($agencyId, $row['id']);
            $row['is_unlocked'] = $unlocked;
            if (! $unlocked) {
                unset($row['email'], $row['phone']);
            }
        }

        return $rows;
    }

    /**
     * Records an unlock and debits the agency's credit balance atomically
     * via CreditService (row-locked — see 05-saas-architecture.md §3).
     * Throws InsufficientCreditsException if the agency can't cover the
     * cost; the controller surfaces that as a "buy more credits" prompt.
     */
    public function unlock(int $agencyId, int $recruiterId, int $candidateProfileId, string $unlockType): array
    {
        if ($this->unlocks->hasUnlocked($agencyId, $candidateProfileId, $unlockType)) {
            return ['already_unlocked' => true];
        }

        $costAction = $unlockType === 'full_profile' ? 'candidate_unlock' : $unlockType;
        $cost = $this->credits->costFor($costAction);

        $id = $this->unlocks->insert([
            'agency_id'            => $agencyId,
            'recruiter_id'         => $recruiterId,
            'candidate_profile_id' => $candidateProfileId,
            'unlock_type'          => $unlockType,
            'credits_spent'        => $cost,
            'unlocked_at'          => date('Y-m-d H:i:s'),
        ], true);

        // Debit AFTER the unlock row exists so it has a reference_id to
        // point at; if the debit throws (insufficient credits), the caller
        // should roll back by deleting the unlock row — handled below via
        // try/catch so a failed debit never leaves a "free" unlock behind.
        try {
            $this->credits->debit($agencyId, $cost, 'candidate_unlocks', $id, $recruiterId);
        } catch (\App\Exceptions\InsufficientCreditsException $e) {
            $this->unlocks->delete($id);
            throw $e;
        }

        $this->activity->log($recruiterId, $agencyId, null, $unlockType === 'resume_download' ? 'resume_download' : 'contact_reveal', [
            'candidate_profile_id' => $candidateProfileId,
        ]);

        return ['unlock_id' => $id, 'already_unlocked' => false, 'credits_spent' => $cost];
    }

    public function save(int $agencyId, int $recruiterId, int $candidateProfileId): void
    {
        if (! $this->saved->isSaved($recruiterId, $candidateProfileId)) {
            $this->saved->insert([
                'agency_id'            => $agencyId,
                'recruiter_id'         => $recruiterId,
                'candidate_profile_id' => $candidateProfileId,
                'created_at'           => date('Y-m-d H:i:s'),
            ]);
        }
    }

    public function tag(int $agencyId, int $candidateProfileId, string $tagName, string $color = '#6c757d'): void
    {
        $tagId = $this->tags->findOrCreate($agencyId, $tagName, $color);
        $this->tags->attachToCandidate($tagId, $candidateProfileId, $agencyId);
    }

    public function createShortlist(int $agencyId, int $createdByUserId, string $name, ?int $jobId = null): int
    {
        return $this->shortlists->insert([
            'agency_id'          => $agencyId,
            'created_by_user_id' => $createdByUserId,
            'name'               => $name,
            'job_id'             => $jobId,
            'created_at'         => date('Y-m-d H:i:s'),
        ], true);
    }

    public function addToShortlist(int $shortlistId, int $candidateProfileId): void
    {
        $this->shortlists->addCandidate($shortlistId, $candidateProfileId);
    }
}
