<?php

namespace App\Http\Controllers\Web;

use App\Http\Controllers\Controller;
use App\Models\Applicant;
use App\Models\JobPosting;
use App\Models\User;
use App\Services\Recruitment\ApplicantScopeService;
use App\Services\Recruitment\ApplicantStageService;
use App\Services\Recruitment\RecruitmentHomeService;
use App\Services\Recruitment\RecruitmentNavService;
use App\Services\Recruitment\RecruitmentSlaService;
use App\Services\Recruitment\ReferralDuplicateService;
use App\Support\EmployeeValidation;
use App\Support\FormTextRules;
use App\Support\ResolvesPrivateUploads;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator as ValidatorInstance;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

class ApplicantController extends Controller
{
    use ResolvesPrivateUploads;

    public function __construct(
        protected ApplicantScopeService $scope,
        protected ApplicantStageService $stageService,
        protected RecruitmentHomeService $home,
        protected ReferralDuplicateService $duplicates,
        protected RecruitmentSlaService $sla
    ) {}

    public function index(Request $request)
    {
        $this->authorize('viewAny', Applicant::class);
        abort_unless($this->scope->canViewFullPipeline($request->user()), 403);

        if ($request->query('view') === 'board') {
            return view('recruitment.applicants.board', [
                'boardGroups' => $this->home->pipelineBoardGroups(),
            ]);
        }

        $search = trim((string) $request->query('search'));
        $stage = (string) $request->query('stage', '');
        $source = (string) $request->query('source', '');
        $referrerId = (int) $request->query('referrer_id');
        $jobPostingId = (int) $request->query('job_posting_id');
        $sla = (string) $request->query('sla', '');

        $applicants = Applicant::with(['jobPosting', 'referredBy'])
            ->when($search !== '', function ($query) use ($search) {
                $query->where(function ($q) use ($search) {
                    $q->where('name', 'like', '%'.$search.'%')
                        ->orWhere('email', 'like', '%'.$search.'%');
                });
            })
            ->when($stage !== '', fn ($q) => $q->where('stage', $stage))
            ->when($source === 'referral', fn ($q) => $q->where('source', 'referral'))
            ->when($source === 'careers', fn ($q) => $q->where('source', 'careers'))
            ->when($source === 'direct', fn ($q) => $q->where(function ($q2) {
                $q2->where('source', '!=', 'referral')->orWhereNull('source');
            }))
            ->when($referrerId > 0, fn ($q) => $q->where('referred_by_user_id', $referrerId))
            ->when($jobPostingId > 0, fn ($q) => $q->where('job_posting_id', $jobPostingId))
            ->when($sla === 'overdue', fn ($q) => $q->whereIn('stage', ['applied', 'screening'])->whereNotNull('sla_due_at')->where('sla_due_at', '<=', now()))
            ->latest()
            ->paginate(15)
            ->withQueryString();

        $totalCount = Applicant::count();
        $stageCounts = [
            'applied' => Applicant::where('stage', 'applied')->count(),
            'screening' => Applicant::where('stage', 'screening')->count(),
            'interview' => Applicant::where('stage', 'interview')->count(),
            'offer' => Applicant::where('stage', 'offer')->count(),
            'rejected' => Applicant::where('stage', 'rejected')->count(),
        ];

        $referrers = User::query()
            ->whereIn('id', Applicant::query()->whereNotNull('referred_by_user_id')->distinct()->pluck('referred_by_user_id'))
            ->orderBy('name')
            ->get(['id', 'name']);

        $jobPostings = JobPosting::orderBy('title')->get(['id', 'title']);

        return view('recruitment.applicants.index', compact(
            'applicants',
            'search',
            'stage',
            'source',
            'referrerId',
            'jobPostingId',
            'totalCount',
            'stageCounts',
            'referrers',
            'jobPostings'
        ));
    }

    public function refer(Request $request)
    {
        $this->assertReferralEligible();

        $jobPostings = JobPosting::where('status', 'active')->orderBy('title')->get();
        $selectedJobId = (int) $request->query('job_posting_id') ?: null;

        return view('recruitment.applicants.refer', compact('jobPostings', 'selectedJobId'));
    }

    public function storeRefer(Request $request)
    {
        $this->assertReferralEligible();

        $companyId = Auth::user()?->company_id;
        $request->merge(EmployeeValidation::normalize($request->all()));
        $validated = $request->validate([
            'job_posting_id' => ['required', Rule::exists('job_postings', 'id')->where('company_id', $companyId)],
            'name' => ['required', 'string', 'max:191', 'regex:'.EmployeeValidation::ALPHA_SPACES],
            'email' => ['nullable', 'email', 'max:191'],
            'phone' => ['nullable', 'digits:10'],
            'notes' => FormTextRules::notNumericOnly(false, 2000),
            'resume' => ['required', 'file', 'mimes:pdf,doc,docx', 'max:5120'],
        ], $this->applicantMessages());

        if ($this->duplicates->isDuplicate((int) $companyId, (int) $validated['job_posting_id'], $validated['email'] ?? null)) {
            return back()->withInput()->withErrors([
                'email' => 'This candidate already has an active application for this role.',
            ]);
        }

        $resumePath = $request->file('resume')->store('resumes/'.$companyId, $this->privateUploadDisk());

        $appliedAt = now();
        $applicant = Applicant::create([
            'company_id' => $companyId,
            'job_posting_id' => $validated['job_posting_id'],
            'referred_by_user_id' => Auth::id(),
            'source' => 'referral',
            'name' => $validated['name'],
            'email' => $validated['email'] ?? null,
            'phone' => $validated['phone'] ?? null,
            'stage' => 'applied',
            'applied_at' => $appliedAt->toDateString(),
            'sla_due_at' => $this->sla->dueAtForNewApplicant($appliedAt),
            'notes' => $validated['notes'] ?? null,
            'resume_path' => $resumePath,
        ]);

        $this->stageService->recordInitial($applicant, Auth::user());

        return redirect()->route('recruitment.my-referrals.show', $applicant)
            ->with('success', 'Referral submitted successfully. You can track progress here.');
    }

    public function create()
    {
        $this->authorize('create', Applicant::class);

        $jobPostings = JobPosting::where('status', 'active')->orderBy('title')->get();

        return view('recruitment.applicants.create', compact('jobPostings'));
    }

    public function store(Request $request)
    {
        $this->authorize('create', Applicant::class);

        $companyId = Auth::user()?->company_id;
        $request->merge(EmployeeValidation::normalize($request->all()));
        $validated = $this->validateApplicant($request, $this->applicantRules(resumeRequired: true));

        $resumePath = null;
        if ($request->hasFile('resume')) {
            $resumePath = $request->file('resume')->store('resumes/'.$companyId, $this->privateUploadDisk());
        }

        $appliedAt = isset($validated['applied_at']) ? Carbon::parse($validated['applied_at']) : now();

        $data = collect($validated)->except(['resume'])->merge([
            'company_id' => $companyId,
            'source' => 'direct',
            'sla_due_at' => $this->sla->dueAtForNewApplicant($appliedAt),
            'resume_path' => $resumePath,
        ])->all();

        $applicant = Applicant::create($data);
        $this->stageService->recordInitial($applicant, Auth::user());

        return redirect()->route('applicants.index')->with('success', 'Applicant added successfully.');
    }

    public function show(Applicant $applicant)
    {
        $this->authorize('view', $applicant);

        $applicant->load(['jobPosting', 'interviews.interviewer', 'referredBy', 'stageEvents.changedBy']);

        return view('recruitment.applicants.show', compact('applicant'));
    }

    public function edit(Applicant $applicant)
    {
        $this->authorize('update', $applicant);

        $jobPostings = JobPosting::orderBy('title')->get();

        return view('recruitment.applicants.edit', compact('applicant', 'jobPostings'));
    }

    public function update(Request $request, Applicant $applicant)
    {
        $this->authorize('update', $applicant);

        $companyId = Auth::user()?->company_id;
        $request->merge(EmployeeValidation::normalize($request->all()));
        $validated = $this->validateApplicant(
            $request,
            $this->applicantRules(
                resumeRequired: empty($applicant->resume_path),
                companyId: $companyId,
                includeReferralFields: true
            )
        );

        if (empty($applicant->resume_path) && ! $request->hasFile('resume')) {
            return back()->withInput()->withErrors([
                'resume' => FormTextRules::messages()['resume.required'],
            ]);
        }

        $previousStage = (string) $applicant->stage;
        $data = collect($validated)->except(['resume', 'note_referrer'])->all();

        if (! $applicant->isReferral()) {
            unset($data['referral_bonus_amount'], $data['referral_bonus_status']);
        }

        if ($request->hasFile('resume')) {
            $this->deletePrivateUpload($applicant->resume_path);
            $data['resume_path'] = $request->file('resume')->store('resumes/'.Auth::user()?->company_id, $this->privateUploadDisk());
        }

        $applicant->update($data);

        if (! in_array($applicant->stage, ['applied', 'screening'], true)) {
            $applicant->update(['sla_due_at' => null]);
        }

        if ($previousStage !== $applicant->stage) {
            $this->stageService->recordChange(
                $applicant,
                $previousStage,
                (string) $applicant->stage,
                Auth::user(),
                noteInternal: null,
                noteReferrer: $validated['note_referrer'] ?? $this->defaultReferrerNote($applicant->stage)
            );
        }

        return redirect()->route('applicants.show', $applicant)->with('success', 'Applicant updated successfully.');
    }

    public function updateStage(Request $request, Applicant $applicant)
    {
        $this->authorize('update', $applicant);

        $validated = $request->validate([
            'stage' => ['required', Rule::in(['applied', 'screening', 'interview', 'offer', 'rejected'])],
            'note_referrer' => ['nullable', 'string', 'max:2000'],
        ]);

        $previousStage = (string) $applicant->stage;
        $applicant->update(['stage' => $validated['stage']]);

        if (! in_array($applicant->stage, ['applied', 'screening'], true)) {
            $applicant->update(['sla_due_at' => null]);
        }

        if ($previousStage !== $applicant->stage) {
            $this->stageService->recordChange(
                $applicant,
                $previousStage,
                (string) $applicant->stage,
                Auth::user(),
                noteInternal: null,
                noteReferrer: $validated['note_referrer'] ?? $this->defaultReferrerNote($applicant->stage)
            );
        }

        if ($request->expectsJson()) {
            return response()->json(['ok' => true, 'stage' => $applicant->stage]);
        }

        return back()->with('success', 'Stage updated.');
    }

    public function destroy(Applicant $applicant)
    {
        $this->authorize('delete', $applicant);

        $this->deletePrivateUpload($applicant->resume_path);
        $applicant->delete();

        return redirect()->route('applicants.index')->with('success', 'Applicant deleted successfully.');
    }

    public function downloadResume(Applicant $applicant): BinaryFileResponse
    {
        $this->authorize('downloadResume', $applicant);

        abort_if(! $applicant->resume_path || ! $this->privateUploadExists($applicant->resume_path), 404);

        return response()->download(
            $this->privateUploadAbsolutePath($applicant->resume_path),
            ($applicant->name ?: 'resume').' — Resume.'.pathinfo($applicant->resume_path, PATHINFO_EXTENSION)
        );
    }

    private function assertReferralEligible(): void
    {
        abort_unless(
            app(RecruitmentNavService::class)->canRefer(Auth::user()),
            403,
            'Only employees can submit referrals.'
        );
    }

    protected function defaultReferrerNote(string $stage): ?string
    {
        if (! in_array($stage, ['screening', 'interview', 'offer', 'rejected'], true)) {
            return null;
        }

        return app(\App\Services\Recruitment\ReferralProgressService::class)->staffLabel($stage);
    }

    /**
     * @return array<string, mixed>
     */
    protected function applicantRules(bool $resumeRequired, ?int $companyId = null, bool $includeReferralFields = false): array
    {
        $jobPostingRule = $companyId
            ? ['required', Rule::exists('job_postings', 'id')->where('company_id', $companyId)]
            : ['required', 'exists:job_postings,id'];

        $rules = [
            'job_posting_id' => $jobPostingRule,
            'name' => ['required', 'string', 'max:191', 'regex:'.EmployeeValidation::ALPHA_SPACES],
            'email' => ['nullable', 'email', 'max:191'],
            'phone' => ['nullable', 'digits:10'],
            'stage' => ['required', Rule::in(['applied', 'screening', 'interview', 'offer', 'rejected'])],
            'rating' => ['nullable', 'numeric', 'min:0', 'max:5'],
            'applied_at' => ['nullable', 'date'],
            'notes' => FormTextRules::notNumericOnly(false, 5000),
            'resume' => array_values(array_filter([
                $resumeRequired ? 'required' : 'nullable',
                'file',
                'mimes:pdf,doc,docx',
                'max:5120',
            ])),
        ];

        if ($includeReferralFields) {
            $rules['note_referrer'] = ['nullable', 'string', 'max:2000'];
            $rules['referral_bonus_amount'] = ['nullable', 'numeric', 'min:0'];
            $rules['referral_bonus_status'] = ['nullable', Rule::in(['pending', 'paid', 'cancelled'])];
        }

        return $rules;
    }

    /**
     * @return array<string, string>
     */
    protected function applicantMessages(): array
    {
        return array_merge(FormTextRules::messages(), [
            'name.regex' => EmployeeValidation::messages()['name.regex'],
            'phone.digits' => EmployeeValidation::messages()['phone.digits'],
        ]);
    }

    /**
     * @param  array<string, mixed>  $rules
     * @return array<string, mixed>
     */
    protected function validateApplicant(Request $request, array $rules): array
    {
        $validator = Validator::make($request->all(), $rules, $this->applicantMessages());
        $validator->after(function (ValidatorInstance $validator) use ($request): void {
            $this->assertAppliedAtNotBeforeJobPostedAt($validator, $request);
        });

        return $validator->validate();
    }

    protected function assertAppliedAtNotBeforeJobPostedAt(ValidatorInstance $validator, Request $request): void
    {
        $jobPostingId = $request->input('job_posting_id');
        $appliedAt = $request->input('applied_at');

        if (! $jobPostingId || ! $appliedAt) {
            return;
        }

        $job = JobPosting::query()->find($jobPostingId);
        if (! $job || ! $job->posted_at) {
            return;
        }

        if (Carbon::parse($appliedAt)->startOfDay()->lt($job->posted_at->copy()->startOfDay())) {
            $validator->errors()->add(
                'applied_at',
                FormTextRules::messages()['applied_at.after_or_equal']
            );
        }
    }
}
