<?php
/**
 * Organization-node builder (single source of truth, since 2.3.1).
 *
 * Replaces the three divergent builders that lived in
 *   - class-bw-schema-renderer.php (homepage Organization path)
 *   - class-bw-schema-renderer.php (fallback WebSite publisher inline)
 *   - class-bw-schema-organization.php (per-post Organization, different field names)
 *
 * Read order for any field:
 *   1. `bw_schema_organization` (array option — the wizard's main blob)
 *   2. Top-level options seeded by the wizard / settings page
 *      (e.g. `bw_schema_org_description`, `bw_schema_founding_date`)
 *   3. Computed values (employee[], founder[] from the team CPT)
 *
 * Everything is keyed by a stable `@id = home_url('/#organization')` so the
 * homepage can emit a connected `@graph` referencing the org from WebSite.publisher,
 * WebPage.about/isPartOf, and Person.worksFor.
 *
 * @package BW_AI_Schema_Pro
 * @since 2.3.1
 */

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

class BW_Schema_Org_Builder {

	/**
	 * Stable @id used to reference the Organization from elsewhere in the graph.
	 */
	public static function id() {
		return home_url( '/' ) . '#organization';
	}

	/**
	 * Same @id pattern but as a tiny reference node — drop this into
	 * Person.worksFor, WebSite.publisher, WebPage.about, etc.
	 */
	public static function reference() {
		return array( '@id' => self::id() );
	}

	/**
	 * Stable @id for the logo ImageObject, so Organization.image and
	 * Organization.logo can both point at the same node.
	 */
	public static function logo_id() {
		return home_url( '/' ) . '#logo';
	}

	/**
	 * Build a complete Organization (or subtype) node.
	 *
	 * The returned array does NOT include `@context` — callers wrapping it in
	 * an `@graph` should hold a single context at the top level.
	 *
	 * @param array $args Optional. Keys:
	 *   - include_context (bool, default false) — add @context.
	 *   - include_employees (bool, default true) — include employee[] array.
	 *   - include_founders (bool, default true) — include founder[] from team flags.
	 * @return array
	 */
	public static function build( array $args = array() ) {
		$args = array_merge( array(
			'include_context'  => false,
			'include_employees' => true,
			'include_founders'  => true,
		), $args );

		$org_data = BW_Schema_Core::get_organization_schema();
		if ( ! is_array( $org_data ) ) {
			$org_data = array();
		}

		$type = self::resolve_type();
		$name = self::pull( $org_data, 'name', get_bloginfo( 'name' ) );

		$node = array(
			'@type' => $type,
			'@id'   => self::id(),
			'name'  => $name,
			'url'   => home_url( '/' ),
		);
		if ( $args['include_context'] ) {
			$node = array_merge( array( '@context' => 'https://schema.org' ), $node );
		}

		// alternateName / legalName / slogan / description ---------------------
		$alt = trim( (string) self::pull( $org_data, 'alternateName', '' ) );
		if ( '' !== $alt ) {
			$node['alternateName'] = $alt;
		}
		$legal = trim( (string) self::pull( $org_data, 'legalName', '' ) );
		if ( '' !== $legal ) {
			$node['legalName'] = $legal;
		}
		$slogan = trim( (string) self::pull( $org_data, 'slogan', '' ) );
		if ( '' !== $slogan ) {
			$node['slogan'] = $slogan;
		}
		$description = trim( (string) get_option( 'bw_schema_org_description', '' ) );
		if ( '' !== $description ) {
			$node['description'] = $description;
		}

		// Identifiers ---------------------------------------------------------
		// Schema.org primary: taxID, vatID, duns, leiCode, naics, iso6523Code.
		foreach ( array( 'taxID', 'vatID', 'duns', 'leiCode', 'naics', 'iso6523Code' ) as $key ) {
			$value = trim( (string) self::pull( $org_data, $key, '' ) );
			if ( '' !== $value ) {
				$node[ $key ] = $value;
			}
		}

		// Logo + image (Organization.image = same ImageObject as logo) --------
		$logo_url = self::pull( $org_data, 'logo', '' );
		if ( ! empty( $logo_url ) ) {
			$logo_node = array(
				'@type' => 'ImageObject',
				'@id'   => self::logo_id(),
				'url'   => $logo_url,
			);
			// Pull dimensions from the attachment if we can resolve it.
			$attach = attachment_url_to_postid( (string) $logo_url );
			if ( $attach ) {
				$meta = wp_get_attachment_metadata( $attach );
				if ( ! empty( $meta['width'] ) ) {
					$logo_node['width'] = (int) $meta['width'];
				}
				if ( ! empty( $meta['height'] ) ) {
					$logo_node['height'] = (int) $meta['height'];
				}
				$caption = get_post_field( 'post_excerpt', $attach );
				if ( '' === trim( (string) $caption ) ) {
					$caption = $name;
				}
				$logo_node['caption'] = $caption;
			} else {
				$logo_node['caption'] = $name;
			}
			$node['logo']  = $logo_node;
			$node['image'] = array( '@id' => self::logo_id() );
		}

		// Contact (top-level + contactPoint[]) --------------------------------
		$tel = self::pull( $org_data, 'telephone', '' );
		if ( ! empty( $tel ) ) {
			$node['telephone'] = $tel;
		}
		$email = self::pull( $org_data, 'email', '' );
		if ( ! empty( $email ) ) {
			$node['email'] = $email;
		}
		$contact_points = get_option( 'bw_schema_contact_points', array() );
		if ( ! empty( $contact_points ) && is_array( $contact_points ) ) {
			$cps = array();
			foreach ( $contact_points as $cp ) {
				if ( empty( $cp['telephone'] ) && empty( $cp['email'] ) ) {
					continue;
				}
				$entry = array( '@type' => 'ContactPoint' );
				if ( ! empty( $cp['type'] ) ) {
					$entry['contactType'] = $cp['type'];
				}
				if ( ! empty( $cp['telephone'] ) ) {
					$entry['telephone'] = $cp['telephone'];
				}
				if ( ! empty( $cp['email'] ) ) {
					$entry['email'] = $cp['email'];
				}
				if ( ! empty( $cp['languages'] ) ) {
					$entry['availableLanguage'] = $cp['languages'];
				}
				$cps[] = $entry;
			}
			if ( ! empty( $cps ) ) {
				$node['contactPoint'] = $cps;
			}
		}

		// Address -------------------------------------------------------------
		$address = get_option( 'bw_schema_address', array() );
		if ( ! empty( $address ) && is_array( $address ) && ! empty( $address['street'] ) ) {
			$postal = array( '@type' => 'PostalAddress' );
			$map    = array(
				'street'  => 'streetAddress',
				'city'    => 'addressLocality',
				'state'   => 'addressRegion',
				'postal'  => 'postalCode',
				'country' => 'addressCountry',
			);
			foreach ( $map as $src => $dst ) {
				if ( ! empty( $address[ $src ] ) ) {
					$postal[ $dst ] = $address[ $src ];
				}
			}
			$node['address'] = $postal;
		}

		// sameAs --------------------------------------------------------------
		$same_as = self::pull( $org_data, 'sameAs', array() );
		if ( ! empty( $same_as ) && is_array( $same_as ) ) {
			$node['sameAs'] = array_values( array_filter( $same_as ) );
		}

		// Founding date -------------------------------------------------------
		$founding_date = get_option( 'bw_schema_founding_date', '' );
		if ( empty( $founding_date ) ) {
			$founding_date = get_option( 'bw_schema_org_founding_date', '' );
		}
		if ( ! empty( $founding_date ) ) {
			$node['foundingDate'] = $founding_date;
		}

		// Founding location ---------------------------------------------------
		$founding_location = trim( (string) self::pull( $org_data, 'foundingLocation', '' ) );
		if ( '' !== $founding_location ) {
			$node['foundingLocation'] = array(
				'@type' => 'Place',
				'name'  => $founding_location,
			);
		}

		// numberOfEmployees as QuantitativeValue -------------------------------
		$emp_count = get_option( 'bw_schema_employee_count', 0 );
		if ( ! empty( $emp_count ) && (int) $emp_count > 0 ) {
			$node['numberOfEmployees'] = array(
				'@type' => 'QuantitativeValue',
				'value' => (int) $emp_count,
			);
		}

		// areaServed — for ALL org types (was previously only on Government) --
		$area = self::pull( $org_data, 'areaServed', array() );
		if ( ! empty( $area ) && is_array( $area ) ) {
			$cleaned = array_values( array_filter( array_map( 'trim', $area ) ) );
			if ( ! empty( $cleaned ) ) {
				$node['areaServed'] = $cleaned;
			}
		}

		// makesOffer ----------------------------------------------------------
		$offers = self::pull( $org_data, 'makesOffer', array() );
		if ( ! empty( $offers ) && is_array( $offers ) ) {
			$out = array();
			foreach ( $offers as $offer ) {
				if ( is_string( $offer ) ) {
					$offer = trim( $offer );
					if ( '' === $offer ) {
						continue;
					}
					$out[] = array(
						'@type'       => 'Offer',
						'itemOffered' => array( '@type' => 'Service', 'name' => $offer ),
					);
				} elseif ( is_array( $offer ) ) {
					$out[] = $offer;
				}
			}
			if ( ! empty( $out ) ) {
				$node['makesOffer'] = $out;
			}
		}

		// Awards / Credentials -------------------------------------------------
		$awards = get_option( 'bw_schema_awards', array() );
		if ( ! empty( $awards ) && is_array( $awards ) ) {
			$awards = array_values( array_filter( $awards ) );
			if ( ! empty( $awards ) ) {
				$node['award'] = $awards;
			}
		}
		$creds = get_option( 'bw_schema_certifications', array() );
		if ( ! empty( $creds ) && is_array( $creds ) ) {
			$creds = array_values( array_filter( $creds ) );
			if ( ! empty( $creds ) ) {
				$node['hasCredential'] = $creds;
			}
		}

		// Local-business extras (subtype hierarchy includes LocalBusiness +
		// EmploymentAgency, MedicalBusiness, etc.) ---------------------------
		if ( self::is_local_business_type( $type ) ) {
			// geo
			$geo = self::pull( $org_data, 'geo', array() );
			if ( is_array( $geo ) && ( ! empty( $geo['latitude'] ) || ! empty( $geo['longitude'] ) ) ) {
				$node['geo'] = array(
					'@type'     => 'GeoCoordinates',
					'latitude'  => isset( $geo['latitude'] ) ? (float) $geo['latitude'] : 0,
					'longitude' => isset( $geo['longitude'] ) ? (float) $geo['longitude'] : 0,
				);
			}
			// openingHoursSpecification
			$hours = self::pull( $org_data, 'openingHours', array() );
			if ( is_array( $hours ) && ! empty( $hours ) ) {
				$specs = array();
				foreach ( $hours as $row ) {
					if ( empty( $row['days'] ) || empty( $row['opens'] ) || empty( $row['closes'] ) ) {
						continue;
					}
					$specs[] = array(
						'@type'      => 'OpeningHoursSpecification',
						'dayOfWeek'  => $row['days'],
						'opens'      => $row['opens'],
						'closes'     => $row['closes'],
					);
				}
				if ( ! empty( $specs ) ) {
					$node['openingHoursSpecification'] = $specs;
				}
			}
			// priceRange
			$price = get_option( 'bw_schema_price_range', '' );
			if ( ! empty( $price ) ) {
				$node['priceRange'] = $price;
			}
		}

		// Founder / employees from team CPT -----------------------------------
		if ( $args['include_founders'] ) {
			$founders = self::collect_founders();
			if ( ! empty( $founders ) ) {
				$node['founder'] = count( $founders ) === 1 ? $founders[0] : $founders;
			}
		}
		if ( $args['include_employees'] ) {
			$employees = self::collect_employees();
			if ( ! empty( $employees ) ) {
				$node['employee'] = $employees;
			}
		}

		return apply_filters( 'bw_schema_org_builder_node', $node, $args );
	}

	/**
	 * Resolve the Organization @type. Falls back to 'Organization' if nothing is set.
	 */
	public static function resolve_type() {
		$type = trim( (string) get_option( 'bw_schema_schema_org_type', '' ) );
		if ( '' === $type ) {
			$type = trim( (string) get_option( 'bw_schema_business_type', '' ) );
		}
		if ( '' === $type ) {
			$type = 'Organization';
		}
		return $type;
	}

	/**
	 * Whether the resolved @type is a LocalBusiness (or subtype thereof).
	 * Used to decide whether to emit geo / openingHours / priceRange.
	 *
	 * (Conservative list — Schema.org's full LocalBusiness subtype hierarchy
	 * has hundreds of entries. We accept anything explicitly marked
	 * LocalBusiness, EmploymentAgency, or one of a few common biz types.)
	 */
	private static function is_local_business_type( $type ) {
		$local_business_types = apply_filters( 'bw_schema_local_business_types', array(
			'LocalBusiness', 'EmploymentAgency', 'ProfessionalService', 'ConsultingBusiness',
			'AccountingService', 'AutoDealer', 'AutoRepair', 'BeautySalon', 'Bakery',
			'Brewery', 'CafeOrCoffeeShop', 'ChildCare', 'Dentist', 'DryCleaningOrLaundry',
			'ElectricalContractor', 'EmploymentAgency', 'EntertainmentBusiness',
			'EventVenue', 'FinancialService', 'FoodEstablishment', 'GeneralContractor',
			'HVACBusiness', 'HealthAndBeautyBusiness', 'HomeAndConstructionBusiness',
			'HousePainter', 'InsuranceAgency', 'LegalService', 'Library', 'LodgingBusiness',
			'MedicalBusiness', 'MedicalClinic', 'MovingCompany', 'NotaryPublic',
			'Notary', 'Optician', 'PetStore', 'Physician', 'Plumber', 'PoliceStation',
			'PostOffice', 'Restaurant', 'RealEstateAgent', 'RoofingContractor', 'School',
			'SelfStorage', 'ShoppingCenter', 'SportsActivityLocation', 'Store',
			'TattooParlor', 'TaxiService', 'TelevisionStation', 'TouristInformationCenter',
			'TravelAgency', 'Winery', 'WholesaleStore',
		) );
		return in_array( $type, $local_business_types, true );
	}

	/**
	 * Pull a value from $org_data with a top-level option fallback.
	 */
	private static function pull( $org_data, $key, $default ) {
		if ( is_array( $org_data ) && isset( $org_data[ $key ] ) && '' !== $org_data[ $key ] ) {
			return $org_data[ $key ];
		}
		return $default;
	}

	/* ---------------- Team-CPT-derived nodes ---------------- */

	/**
	 * Founders: team CPT entries flagged is_leader, with the exclude-from-schema
	 * flag respected.
	 */
	public static function collect_founders() {
		$team_post_type = class_exists( 'BW_Schema_Team_Member' )
			? BW_Schema_Team_Member::get_team_post_type()
			: get_option( 'bw_schema_team_post_type', '' );
		if ( ! $team_post_type ) {
			return array();
		}

		$ids = get_posts( array(
			'post_type'      => $team_post_type,
			'posts_per_page' => -1,
			'post_status'    => 'publish',
			'fields'         => 'ids',
			'meta_query'     => array(
				array(
					'key'     => '_bw_schema_is_leader',
					'value'   => '1',
					'compare' => '=',
				),
			),
		) );

		$out = array();
		foreach ( $ids as $pid ) {
			if ( self::is_excluded( $pid ) ) {
				continue;
			}
			$out[] = self::person_node_for_member( $pid );
		}
		return $out;
	}

	/**
	 * Employees: every published team CPT entry, sans excluded placeholders.
	 */
	public static function collect_employees() {
		$team_post_type = class_exists( 'BW_Schema_Team_Member' )
			? BW_Schema_Team_Member::get_team_post_type()
			: get_option( 'bw_schema_team_post_type', '' );
		if ( ! $team_post_type ) {
			return array();
		}

		$ids = get_posts( array(
			'post_type'      => $team_post_type,
			'posts_per_page' => -1,
			'post_status'    => 'publish',
			'orderby'        => 'menu_order title',
			'order'          => 'ASC',
			'fields'         => 'ids',
		) );

		$out = array();
		foreach ( $ids as $pid ) {
			if ( self::is_excluded( $pid ) ) {
				continue;
			}
			$out[] = self::person_node_for_member( $pid );
		}
		return $out;
	}

	/**
	 * Per-member exclude flag. Set via the team CPT metabox; honoured everywhere
	 * the org builder collects Person nodes. Lets sites flag placeholder rows
	 * like "Is It You? — Join Our Team!" so they don't get rendered as real
	 * Person schema.
	 */
	public static function is_excluded( $member_id ) {
		return '1' === (string) get_post_meta( (int) $member_id, '_bw_schema_exclude_from_schema', true );
	}

	/**
	 * Person node for a team CPT entry — used inside Organization.employee[]
	 * and Organization.founder[]. Stable @id so the standalone Person page can
	 * be referenced from elsewhere in the graph.
	 */
	private static function person_node_for_member( $member_id ) {
		$member  = get_post( $member_id );
		$node = array(
			'@type' => 'Person',
			'@id'   => get_permalink( $member ) . '#person',
			'name'  => $member->post_title,
			'url'   => get_permalink( $member ),
		);

		// Job title.
		$job = '';
		if ( class_exists( 'BW_Schema_Team_Member' ) ) {
			$detected = BW_Schema_Team_Member::detect_job_title( $member_id );
			$job      = is_array( $detected ) ? ( $detected['value'] ?? '' ) : $detected;
		}
		if ( '' !== $job ) {
			$node['jobTitle'] = $job;
		}

		// Image.
		$image = get_the_post_thumbnail_url( $member, 'thumbnail' );
		if ( ! empty( $image ) ) {
			$node['image'] = $image;
		}

		// worksFor — by @id reference, so the graph stays connected.
		$node['worksFor'] = self::reference();

		return $node;
	}
}
