<?php

namespace App\Services\Job;

use App\Models\JobModel;
use App\Repositories\JobRepository;
use App\Services\Billing\SubscriptionService;
use App\Services\Support\ActivityLogger;
use App\Services\Support\AuditService;

/**
 * Owns job posting business rules: slug generation, publish/close
 * transitions, and plan-limit enforcement via SubscriptionService
 * (see 05-saas-architecture.md §2).
 */
class JobService
{
    private JobRepository $jobs;
    private JobModel $jobModel;
    private SubscriptionService $subscriptions;
    private ActivityLogger $activity;
    private AuditService $audit;

    public function __construct()
    {
        $this->jobModel      = new JobModel();
        $this->jobs          = new JobRepository($this->jobModel);
        $this->subscriptions = new SubscriptionService();
        $this->activity      = new ActivityLogger();
        $this->audit         = new AuditService();
    }

    public function create(int $agencyId, int $postedByUserId, array $data): array
    {
        $this->subscriptions->assertWithinLimit($agencyId, 'jobs_posted_used', 'job_posting_limit');

        $data['agency_id']         = $agencyId;
        $data['posted_by_user_id'] = $postedByUserId;
        $data['seo_slug']          = $this->uniqueSlug($data['title']);
        $data['status']            = $data['status'] ?? 'draft';

        if (! empty($data['required_skills']) && is_array($data['required_skills'])) {
            $data['required_skills'] = json_encode($data['required_skills']);
        }

        $job = $this->jobs->create($data);

        $this->subscriptions->incrementUsage($agencyId, 'jobs_posted_used');
        $this->activity->log($postedByUserId, $agencyId, null, 'job_create', ['job_id' => $job->id]);

        return $this->jobModel->find($job->id)->toArray();
    }

    /**
     * Bulk job creation from a parsed CSV (see BulkJobController::upload()
     * for the parsing step). Runs each row through the exact same create()
     * path as a single job post — same plan-limit enforcement, same
     * seo_slug generation — so there's no separate/inconsistent code path
     * for bulk vs. one-at-a-time posting. Stops early once the plan limit
     * is hit (further rows would all fail the same way) rather than
     * burning through the whole file first.
     */
    public function bulkCreate(int $agencyId, int $postedByUserId, array $rows): array
    {
        $results = ['created' => [], 'skipped_duplicates' => [], 'failed' => []];

        // Existing titles for this agency (case-insensitive) — lets us
        // detect "this exact row was already created" when someone
        // re-uploads a CSV that partially succeeded on a prior attempt
        // (e.g. after hitting a plan limit partway through), rather than
        // creating duplicate job postings for the rows that already went
        // through.
        $existingJobs = $this->jobModel->where('agency_id', $agencyId)->findAll();
        $existingTitles = [];
        foreach ($existingJobs as $existingJob) {
            $existingTitles[] = strtolower(trim($existingJob->title));
        }

        foreach ($rows as $i => $row) {
            $rowNum = $i + 2; // +2 accounts for the header row and 1-based row numbering, matching what a spreadsheet user actually sees

            if (empty($row['title']) || empty($row['description'])) {
                $results['failed'][] = ['row' => $rowNum, 'title' => $row['title'] ?? '(blank)', 'error' => 'Title and description are required.'];
                continue;
            }
            if (mb_strlen($row['description']) < 50) {
                $results['failed'][] = ['row' => $rowNum, 'title' => $row['title'], 'error' => 'Description must be at least 50 characters.'];
                continue;
            }
            if (in_array(strtolower(trim($row['title'])), $existingTitles, true)) {
                $results['skipped_duplicates'][] = ['row' => $rowNum, 'title' => $row['title'], 'note' => 'A job with this exact title already exists for your agency — skipped to avoid a duplicate. Rename it in the CSV if this is intentionally a different posting.'];
                continue;
            }

            $payload = [
                'title'                  => $row['title'],
                'description'            => $row['description'],
                'department'             => $row['department'] ?: null,
                'employment_type'        => $this->normalizeEnum($row['employment_type'] ?? '', ['full_time', 'part_time', 'contract', 'internship', 'freelance'], 'full_time'),
                'experience_level'       => $this->normalizeEnum($row['experience_level'] ?? '', ['entry', 'mid', 'senior', 'lead', 'executive'], 'mid'),
                'location'               => $row['location'] ?: null,
                'city'                   => $row['city'] ?: null,
                'country'                => $row['country'] ?: null,
                'salary_min'             => is_numeric($row['salary_min'] ?? null) ? $row['salary_min'] : null,
                'salary_max'             => is_numeric($row['salary_max'] ?? null) ? $row['salary_max'] : null,
                'education_requirement'  => $row['education_requirement'] ?? null,
                'required_skills'        => ! empty($row['required_skills']) ? array_filter(array_map('trim', explode(';', $row['required_skills']))) : [],
                'is_remote'              => $this->toBool($row['is_remote'] ?? null),
                'is_hybrid'              => $this->toBool($row['is_hybrid'] ?? null),
                'is_visa_sponsorship'    => $this->toBool($row['is_visa_sponsorship'] ?? null),
                'is_fresh_graduate_ok'   => $this->toBool($row['is_fresh_graduate_ok'] ?? null),
                'is_urgent'              => $this->toBool($row['is_urgent'] ?? null),
                'status'                 => $this->toBool($row['publish'] ?? null) ? 'published' : 'draft',
            ];

            try {
                $job = $this->create($agencyId, $postedByUserId, $payload);
                $results['created'][] = ['row' => $rowNum, 'title' => $job['title'], 'id' => $job['id'], 'status' => $job['status']];
                $existingTitles[] = strtolower(trim($job['title'])); // so a later duplicate row in this same file is also caught
            } catch (\App\Exceptions\PlanLimitExceededException $e) {
                $results['failed'][] = ['row' => $rowNum, 'title' => $row['title'], 'error' => $e->getMessage()];
                break; // every remaining row would hit the same limit — no point continuing
            } catch (\Throwable $e) {
                $results['failed'][] = ['row' => $rowNum, 'title' => $row['title'], 'error' => $e->getMessage()];
            }
        }

        return $results;
    }

    private function normalizeEnum(string $value, array $allowed, string $default): string
    {
        $value = strtolower(trim(str_replace([' ', '-'], '_', $value)));
        return in_array($value, $allowed, true) ? $value : $default;
    }

    private function toBool(?string $value): bool
    {
        return in_array(strtolower(trim((string) $value)), ['1', 'yes', 'true', 'y'], true);
    }

    public function update(int $agencyId, int $jobId, array $data, int $updatedByUserId): array
    {
        $existing = $this->jobs->find($jobId);
        if (! $existing) {
            throw new \RuntimeException('Job not found or not in your agency.');
        }

        $before = $existing->toArray();

        if (! empty($data['required_skills']) && is_array($data['required_skills'])) {
            $data['required_skills'] = json_encode($data['required_skills']);
        }

        unset($data['agency_id'], $data['posted_by_user_id'], $data['seo_slug']); // immutable via this path

        $this->jobs->update($jobId, $data);
        $this->audit->record($updatedByUserId, 'jobs', 'update', $jobId, $before, $data);

        return $this->jobModel->find($jobId)->toArray();
    }

    public function publish(int $agencyId, int $jobId, int $publishedByUserId): array
    {
        $job = $this->jobs->find($jobId);
        if (! $job) {
            throw new \RuntimeException('Job not found or not in your agency.');
        }

        $this->jobs->update($jobId, [
            'status'     => 'published',
            'expires_at' => $job->expires_at ?? date('Y-m-d', strtotime('+30 days')),
        ]);

        $this->activity->log($publishedByUserId, $agencyId, null, 'job_publish', ['job_id' => $jobId]);

        return $this->jobModel->find($jobId)->toArray();
    }

    public function close(int $agencyId, int $jobId, int $closedByUserId): array
    {
        $job = $this->jobs->find($jobId);
        if (! $job) {
            throw new \RuntimeException('Job not found or not in your agency.');
        }

        $this->jobs->update($jobId, ['status' => 'closed']);
        $this->activity->log($closedByUserId, $agencyId, null, 'job_close', ['job_id' => $jobId]);

        return $this->jobModel->find($jobId)->toArray();
    }

    public function delete(int $agencyId, int $jobId, int $deletedByUserId): void
    {
        $job = $this->jobs->find($jobId);
        if (! $job) {
            throw new \RuntimeException('Job not found or not in your agency.');
        }

        $this->jobs->delete($jobId); // soft delete via useSoftDeletes
        $this->audit->record($deletedByUserId, 'jobs', 'delete', $jobId, $job->toArray(), null);
    }

    public function searchPublic(array $filters, int $page = 1, int $perPage = 20): array
    {
        return $this->jobModel->searchPublic($filters, $page, $perPage);
    }

    /**
     * Lists ALL of an agency's own jobs regardless of status (draft,
     * published, closed, expired) — distinct from searchPublic(), which
     * only ever returns published+non-expired jobs for public job-seeker
     * browsing. Agencies need to see and manage their drafts too.
     */
    public function listForAgency(int $agencyId, ?string $status = null, int $page = 1, int $perPage = 50): array
    {
        $builder = $this->jobModel->where('agency_id', $agencyId);
        if ($status) {
            $builder->where('status', $status);
        }
        $offset = ($page - 1) * $perPage;
        $jobs = $builder->orderBy('created_at', 'DESC')->findAll($perPage, $offset);
        return array_map(fn ($j) => $j->toArray(), $jobs);
    }

    public function viewPublicBySlug(string $slug): array
    {
        $job = $this->jobModel->findBySlug($slug);
        if (! $job || ! $job->isPublished()) {
            throw new \RuntimeException('Job not found.');
        }
        $this->jobModel->incrementViews($job->id);
        return $job->toArray();
    }

    private function uniqueSlug(string $title): string
    {
        $base = url_title($title, '-', true) . '-' . bin2hex(random_bytes(3));
        $slug = $base;
        $i = 1;
        while ($this->jobModel->findBySlug($slug) !== null) {
            $slug = $base . '-' . (++$i);
        }
        return $slug;
    }
}
