<?php
/**
 * Data-access layer for work-order line items.
 *
 * Line items are the labor and parts rows that hang off a work order. All reads
 * and writes go through this repository using $wpdb with prepared statements and
 * explicit format arrays; no caller interpolates user input into SQL. The only
 * interpolated identifier is the trusted, prefix-built table name. Money columns
 * (quantity, unit_price) are DECIMAL and bound as strings, never floats.
 *
 * @package WrenchPress
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class WrenchPress_Work_Order_Items_Repository
 */
class WrenchPress_Work_Order_Items_Repository {

	/**
	 * The two line-item types. Every write whitelists item_type against this list
	 * (Security V5) so an unexpected value never reaches the column.
	 *
	 * @return string[] Allowed item-type slugs.
	 */
	public static function item_types() {
		return array( 'labor', 'part' );
	}

	/**
	 * Fully qualified work-order-items table name.
	 *
	 * @return string
	 */
	protected static function table() {
		global $wpdb;

		return $wpdb->prefix . 'wrenchpress_work_order_items';
	}

	/**
	 * Insert a new line item against a work order.
	 *
	 * Rejects a zero work_order_id and any item_type outside the whitelist.
	 * quantity and unit_price are bound as strings because they are DECIMAL
	 * columns; binding as %f would force a float round-trip.
	 *
	 * @param array $data work_order_id, item_type, description, quantity,
	 *                    unit_price, optional position.
	 * @return int|false New item id, or false on a bad work order / type / failure.
	 */
	public static function create( array $data ) {
		global $wpdb;

		$work_order_id = isset( $data['work_order_id'] ) ? absint( $data['work_order_id'] ) : 0;

		if ( 0 === $work_order_id ) {
			return false;
		}

		$item_type = isset( $data['item_type'] ) ? (string) $data['item_type'] : 'labor';
		if ( ! in_array( $item_type, self::item_types(), true ) ) {
			return false;
		}

		$now = current_time( 'mysql' );

		$row = array(
			'work_order_id' => $work_order_id,
			'item_type'     => $item_type,
			'description'   => isset( $data['description'] ) ? (string) $data['description'] : '',
			'quantity'      => isset( $data['quantity'] ) ? (string) $data['quantity'] : '0',
			'unit_price'    => isset( $data['unit_price'] ) ? (string) $data['unit_price'] : '0',
			'position'      => isset( $data['position'] ) ? absint( $data['position'] ) : 0,
			'created_at'    => $now,
			'updated_at'    => $now,
		);

		$formats = array(
			'%d', // work_order_id.
			'%s', // item_type.
			'%s', // description.
			'%s', // quantity (DECIMAL passed as string).
			'%s', // unit_price (DECIMAL passed as string).
			'%d', // position.
			'%s', // created_at.
			'%s', // updated_at.
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table write via the $wpdb array API, intentionally not object-cached.
		$inserted = $wpdb->insert( self::table(), $row, $formats );

		if ( false === $inserted ) {
			return false;
		}

		return (int) $wpdb->insert_id;
	}

	/**
	 * Update an existing line item. Recomputes updated_at.
	 *
	 * Rejects any item_type outside the whitelist. quantity and unit_price are
	 * bound as strings (DECIMAL columns).
	 *
	 * @param int   $id   Line-item id.
	 * @param array $data item_type, description, quantity, unit_price, position.
	 * @return bool True on success.
	 */
	public static function update( $id, array $data ) {
		global $wpdb;

		$id = absint( $id );

		if ( 0 === $id ) {
			return false;
		}

		$item_type = isset( $data['item_type'] ) ? (string) $data['item_type'] : 'labor';
		if ( ! in_array( $item_type, self::item_types(), true ) ) {
			return false;
		}

		$row = array(
			'item_type'   => $item_type,
			'description' => isset( $data['description'] ) ? (string) $data['description'] : '',
			'quantity'    => isset( $data['quantity'] ) ? (string) $data['quantity'] : '0',
			'unit_price'  => isset( $data['unit_price'] ) ? (string) $data['unit_price'] : '0',
			'position'    => isset( $data['position'] ) ? absint( $data['position'] ) : 0,
			'updated_at'  => current_time( 'mysql' ),
		);

		$formats = array( '%s', '%s', '%s', '%s', '%d', '%s' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table write via the $wpdb array API, intentionally not object-cached.
		$result = $wpdb->update( self::table(), $row, array( 'id' => $id ), $formats, array( '%d' ) );

		return false !== $result;
	}

	/**
	 * Delete a single line item by id.
	 *
	 * Line items are working pricing rows (not an audit trail), so a hard delete
	 * is the correct removal here. The id is bound via the format array.
	 *
	 * @param int $id Line-item id.
	 * @return bool True on success.
	 */
	public static function delete( $id ) {
		global $wpdb;

		$id = absint( $id );

		if ( 0 === $id ) {
			return false;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table write via the $wpdb array API, intentionally not object-cached.
		$result = $wpdb->delete( self::table(), array( 'id' => $id ), array( '%d' ) );

		return false !== $result;
	}

	/**
	 * List every line item for one work order, in display position order.
	 *
	 * @param int $work_order_id Work-order id.
	 * @return array List of row objects, ordered by position ascending.
	 */
	public static function list_for_work_order( $work_order_id ) {
		global $wpdb;

		$work_order_id = absint( $work_order_id );

		if ( 0 === $work_order_id ) {
			return array();
		}

		$table = self::table();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table, intentionally not object-cached.
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT * FROM %i WHERE work_order_id = %d ORDER BY position ASC',
				$table,
				$work_order_id
			)
		);

		return is_array( $rows ) ? $rows : array();
	}

	/**
	 * Compute a work order's labor, parts, subtotal, tax, and grand total.
	 *
	 * Totals are derived on read from the current line items (never stored), so
	 * they always reflect the live rows. Each line total is quantity * unit_price;
	 * labor and parts are summed separately; tax is subtotal * tax_rate. No
	 * intermediate value is rounded here. Display code rounds at the edge with
	 * number_format(); rounding mid-computation would accumulate cent drift.
	 *
	 * @param int          $work_order_id Work-order id.
	 * @param float|string $tax_rate      Decimal tax rate (for example 0.0825).
	 * @return array{labor: float, parts: float, subtotal: float, tax: float, grand_total: float}
	 */
	public static function totals( $work_order_id, $tax_rate = 0 ) {
		$items = self::list_for_work_order( $work_order_id );

		$labor = 0.0;
		$parts = 0.0;

		foreach ( $items as $item ) {
			$line = (float) $item->quantity * (float) $item->unit_price;

			if ( 'part' === $item->item_type ) {
				$parts += $line;
			} else {
				$labor += $line;
			}
		}

		$subtotal = $labor + $parts;
		$tax      = $subtotal * (float) $tax_rate;

		return array(
			'labor'       => $labor,
			'parts'       => $parts,
			'subtotal'    => $subtotal,
			'tax'         => $tax,
			'grand_total' => $subtotal + $tax,
		);
	}
}
