<?php

declare(strict_types=1);

/**
 * JobPosting Schema Service
 *
 * Single source of truth for building and rendering JobPosting JSON-LD.
 * Required fields are `title`, `description`, `hiringOrganization` (with a
 * non-empty name), and `datePosted` (ISO 8601 date). The location/salary
 * sub-objects are only emitted when their inner fields are non-empty so the
 * frontend never carries an empty PostalAddress or zero-value MonetaryAmount.
 *
 * @package ProRank\SEO\Core\Services
 * @since 1.3.1
 */

namespace ProRank\SEO\Core\Services;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}


class JobPostingSchemaService {

    private static bool $emitted = false;

    /**
     * Build a JobPosting JSON-LD array.
     *
     * @param array $config Config with keys:
     *     - title              (string, required)
     *     - description        (string, required)
     *     - hiringOrganization (array{name,url?}|string, required)
     *     - datePosted         (string, required, YYYY-MM-DD or full ISO)
     *     - validThrough       (string, optional, YYYY-MM-DD or full ISO)
     *     - employmentType     (string|array<string>, optional)
     *     - jobLocation        (array{streetAddress?, addressLocality?,
     *                           addressRegion?, postalCode?, addressCountry?},
     *                          optional)
     *     - applicantLocationRequirements (string|array<string>, optional, ISO 3166 codes)
     *     - baseSalary         (array{currency, value|minValue+maxValue, unitText?}, optional)
     * @return array|null Schema array, or null when invalid.
     */
    public static function build_schema( array $config ): ?array {
        $title = trim( (string) ( $config['title'] ?? '' ) );
        $description = trim( (string) ( $config['description'] ?? '' ) );
        $date_posted = self::normalize_iso_date( (string) ( $config['datePosted'] ?? '' ) );

        if ( $title === '' || $description === '' || $date_posted === null ) {
            return null;
        }

        $organization = self::normalize_organization( $config['hiringOrganization'] ?? null );
        if ( $organization === null ) {
            return null;
        }

        $schema = [
            '@context'           => 'https://schema.org',
            '@type'              => 'JobPosting',
            'title'              => wp_strip_all_tags( $title ),
            'description'        => wp_kses_post( $description ),
            'datePosted'         => $date_posted,
            'hiringOrganization' => $organization,
        ];

        $valid_through = self::normalize_iso_date( (string) ( $config['validThrough'] ?? '' ) );
        if ( $valid_through !== null ) {
            $schema['validThrough'] = $valid_through;
        }

        $employment = self::normalize_employment_type( $config['employmentType'] ?? null );
        if ( $employment !== null ) {
            $schema['employmentType'] = $employment;
        }

        $job_location = self::normalize_job_location( $config['jobLocation'] ?? null );
        if ( $job_location !== null ) {
            $schema['jobLocation'] = $job_location;
        }

        $applicant_locations = self::normalize_applicant_locations( $config['applicantLocationRequirements'] ?? null );
        if ( ! empty( $applicant_locations ) ) {
            $schema['applicantLocationRequirements'] = count( $applicant_locations ) === 1
                ? $applicant_locations[0]
                : $applicant_locations;
        }

        $base_salary = self::normalize_base_salary( $config['baseSalary'] ?? null );
        if ( $base_salary !== null ) {
            $schema['baseSalary'] = $base_salary;
        }

        return $schema;
    }

    public static function render_script( array $config, string $class_attr = '' ): string {
        $schema = self::build_schema( $config );

        if ( $schema === null ) {
            return '';
        }

        $class = $class_attr !== ''
            ? ' class="' . esc_attr( $class_attr ) . '"'
            : '';

        return '<script type="application/ld+json"' . $class . '>'
            . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
            . '</script>';
    }

    public static function mark_emitted(): void {
        self::$emitted = true;
    }

    public static function was_emitted(): bool {
        return self::$emitted;
    }

    public static function reset_for_tests(): void {
        self::$emitted = false;
    }

    /**
     * Accepts either YYYY-MM-DD or a full ISO 8601 timestamp and returns the
     * canonical date string. Returns null on garbage input.
     */
    private static function normalize_iso_date( string $value ): ?string {
        $value = trim( $value );
        if ( $value === '' ) {
            return null;
        }
        // YYYY-MM-DD or full ISO 8601.
        if ( preg_match( '/^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+\-]\d{2}:?\d{2})?)?$/', $value ) ) {
            return $value;
        }
        return null;
    }

    private static function normalize_organization( $value ): ?array {
        if ( is_string( $value ) ) {
            $value = [ 'name' => $value ];
        }
        if ( ! is_array( $value ) ) {
            return null;
        }
        $name = trim( wp_strip_all_tags( (string) ( $value['name'] ?? '' ) ) );
        if ( $name === '' ) {
            return null;
        }
        $org = [
            '@type' => 'Organization',
            'name'  => $name,
        ];
        $url = trim( (string) ( $value['url'] ?? '' ) );
        if ( $url !== '' ) {
            $clean = esc_url_raw( $url );
            if ( $clean !== '' ) {
                $org['sameAs'] = $clean;
            }
        }
        $logo = trim( (string) ( $value['logo'] ?? '' ) );
        if ( $logo !== '' ) {
            $clean = esc_url_raw( $logo );
            if ( $clean !== '' ) {
                $org['logo'] = $clean;
            }
        }
        return $org;
    }

    private static function normalize_employment_type( $value ) {
        $allowed = [ 'FULL_TIME', 'PART_TIME', 'CONTRACTOR', 'TEMPORARY', 'INTERN', 'VOLUNTEER', 'PER_DIEM', 'OTHER' ];
        if ( is_string( $value ) ) {
            $list = preg_split( '/[,\s]+/', strtoupper( $value ) ) ?: [];
        } elseif ( is_array( $value ) ) {
            $list = array_map( 'strtoupper', array_map( 'strval', $value ) );
        } else {
            return null;
        }
        $list = array_values( array_filter( array_unique( $list ), static fn( $t ) => in_array( $t, $allowed, true ) ) );
        if ( empty( $list ) ) {
            return null;
        }
        return count( $list ) === 1 ? $list[0] : $list;
    }

    /**
     * Build a Place + PostalAddress wrapper. Returns null when no address
     * field has a non-empty value so we never emit an empty PostalAddress.
     */
    private static function normalize_job_location( $value ): ?array {
        if ( ! is_array( $value ) ) {
            return null;
        }
        $fields = [
            'streetAddress'   => trim( (string) ( $value['streetAddress'] ?? '' ) ),
            'addressLocality' => trim( (string) ( $value['addressLocality'] ?? '' ) ),
            'addressRegion'   => trim( (string) ( $value['addressRegion'] ?? '' ) ),
            'postalCode'      => trim( (string) ( $value['postalCode'] ?? '' ) ),
            'addressCountry'  => trim( (string) ( $value['addressCountry'] ?? '' ) ),
        ];
        $non_empty = array_filter( $fields, static fn( $v ) => $v !== '' );
        if ( empty( $non_empty ) ) {
            return null;
        }
        $address = [ '@type' => 'PostalAddress' ];
        foreach ( $non_empty as $k => $v ) {
            $address[ $k ] = wp_strip_all_tags( $v );
        }
        return [
            '@type'   => 'Place',
            'address' => $address,
        ];
    }

    /**
     * Country/region codes. Empty -> null.
     *
     * @return array<int,array{ "@type":string, name:string }>
     */
    private static function normalize_applicant_locations( $value ): array {
        if ( is_string( $value ) ) {
            $list = preg_split( '/[,\s]+/', $value ) ?: [];
        } elseif ( is_array( $value ) ) {
            $list = $value;
        } else {
            return [];
        }
        $out = [];
        foreach ( $list as $item ) {
            $code = strtoupper( trim( (string) $item ) );
            if ( $code === '' ) {
                continue;
            }
            $out[] = [
                '@type' => 'Country',
                'name'  => $code,
            ];
        }
        return $out;
    }

    /**
     * Build a MonetaryAmount + QuantitativeValue. Returns null when no
     * numeric value is present so we never emit a zero-valued salary block.
     */
    private static function normalize_base_salary( $value ): ?array {
        if ( ! is_array( $value ) ) {
            return null;
        }
        $currency = strtoupper( trim( (string) ( $value['currency'] ?? '' ) ) );
        if ( $currency === '' ) {
            return null;
        }

        $exact = isset( $value['value'] ) && trim( (string) $value['value'] ) !== '' ? (float) $value['value'] : null;
        $min = isset( $value['minValue'] ) && trim( (string) $value['minValue'] ) !== '' ? (float) $value['minValue'] : null;
        $max = isset( $value['maxValue'] ) && trim( (string) $value['maxValue'] ) !== '' ? (float) $value['maxValue'] : null;
        $unit = strtoupper( trim( (string) ( $value['unitText'] ?? '' ) ) );

        if ( $exact === null && $min === null && $max === null ) {
            return null;
        }

        $qv = [ '@type' => 'QuantitativeValue' ];
        if ( $exact !== null ) {
            $qv['value'] = $exact;
        }
        if ( $min !== null ) {
            $qv['minValue'] = $min;
        }
        if ( $max !== null ) {
            $qv['maxValue'] = $max;
        }
        if ( in_array( $unit, [ 'HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR' ], true ) ) {
            $qv['unitText'] = $unit;
        }

        return [
            '@type'    => 'MonetaryAmount',
            'currency' => $currency,
            'value'    => $qv,
        ];
    }
}
