<?php
/**
 * KleinBooks Offers (Angebote) REST API
 *
 * Quotes/offers — the step before the invoice in the German freelancer workflow
 * (Anfrage → Angebot → Auftrag → Rechnung). Mirrors the invoice model; the headline
 * feature is convert-to-invoice (one call → a pre-filled draft invoice, cross-linked).
 *
 * Offers are a FREE feature (the workflow must be complete for trial users to evaluate).
 * 'expired' is a derived status (sent + valid_until < today) — no cron, no stored drift.
 */

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

class KleinBooks_Offers_API {

    const NAMESPACE = 'kleinbooks/v1';

    // Stored statuses the client may set via the status endpoint. 'converted' is set only
    // by the convert endpoint; 'expired' is derived, never stored.
    const SETTABLE_STATUSES = array('draft', 'sent', 'accepted', 'declined');

    public static function register_routes() {
        // Reuse the invoice API's permission callback so auth stays in one place.
        $perm = array('KleinBooks_REST_API', 'check_permission');

        register_rest_route(self::NAMESPACE, '/offers', array(
            array('methods' => 'GET',  'callback' => array(__CLASS__, 'get_offers'),   'permission_callback' => $perm),
            array('methods' => 'POST', 'callback' => array(__CLASS__, 'create_offer'),  'permission_callback' => $perm),
        ));

        register_rest_route(self::NAMESPACE, '/offers/(?P<id>\d+)', array(
            array('methods' => 'GET',    'callback' => array(__CLASS__, 'get_offer'),    'permission_callback' => $perm),
            array('methods' => 'PUT',    'callback' => array(__CLASS__, 'update_offer'), 'permission_callback' => $perm),
            array('methods' => 'DELETE', 'callback' => array(__CLASS__, 'delete_offer'), 'permission_callback' => $perm),
        ));

        register_rest_route(self::NAMESPACE, '/offers/(?P<id>\d+)/status', array(
            'methods' => 'PATCH',
            'callback' => array(__CLASS__, 'update_offer_status'),
            'permission_callback' => $perm,
        ));

        register_rest_route(self::NAMESPACE, '/offers/(?P<id>\d+)/send-email', array(
            'methods' => 'POST',
            'callback' => array(__CLASS__, 'send_offer'),
            'permission_callback' => $perm,
        ));

        // The killer endpoint: copy an offer into a new draft invoice, cross-link both.
        register_rest_route(self::NAMESPACE, '/offers/(?P<id>\d+)/convert-to-invoice', array(
            'methods' => 'POST',
            'callback' => array(__CLASS__, 'convert_to_invoice'),
            'permission_callback' => $perm,
        ));

        register_rest_route(self::NAMESPACE, '/offers/next-number', array(
            'methods' => 'GET',
            'callback' => array(__CLASS__, 'get_next_offer_number'),
            'permission_callback' => $perm,
        ));

        register_rest_route(self::NAMESPACE, '/offers/(?P<id>\d+)/pdf/preview', array(
            'methods' => 'GET',
            'callback' => array(__CLASS__, 'preview_offer_pdf'),
            'permission_callback' => $perm,
        ));

        register_rest_route(self::NAMESPACE, '/offers/(?P<id>\d+)/pdf/download', array(
            'methods' => 'GET',
            'callback' => array(__CLASS__, 'download_offer_pdf'),
            'permission_callback' => $perm,
        ));
    }

    /**
     * Email the offer PDF to the client. Delegates to the main class, then returns the
     * (possibly updated) status/sent_at so the UI can flip the pill without a reload.
     */
    public static function send_offer($request) {
        global $wpdb;
        $id = (int)$request['id'];
        $offers_table = KleinBooks_Database::get_table_name('offers');

        $existing = $wpdb->get_row($wpdb->prepare("SELECT status, sent_at FROM $offers_table WHERE id = %d", $id));
        if (!$existing) {
            return new WP_Error('not_found', 'Offer not found', array('status' => 404));
        }

        $email_sent = false;
        $email_error = null;
        try {
            $result = KleinBooks_Main::get_instance()->send_offer_email($id);
            if (is_wp_error($result)) {
                $email_error = $result->get_error_message();
            } else {
                $email_sent = true;
            }
        } catch (Exception $e) {
            $email_error = $e->getMessage();
        }

        // Re-read so we reflect any sent_at/status the send stamped.
        $after = $wpdb->get_row($wpdb->prepare("SELECT status, sent_at FROM $offers_table WHERE id = %d", $id));

        return rest_ensure_response(array(
            'success' => true,
            'email_sent' => $email_sent,
            'email_error' => $email_error,
            'message' => $email_sent
                ? __('Angebot per E-Mail gesendet.', 'kleinbooks')
                : __('E-Mail-Versand fehlgeschlagen (SMTP prüfen).', 'kleinbooks'),
            'status' => $after->status,
            'sent_at' => $after->sent_at,
        ));
    }

    public static function preview_offer_pdf($request) {
        try {
            KleinBooks_PDF_Generator::generate_offer((int)$request['id'], 'stream');
        } catch (\Exception $e) {
            return new WP_Error('pdf_error', $e->getMessage(), array('status' => 500));
        }
        exit;
    }

    public static function download_offer_pdf($request) {
        try {
            KleinBooks_PDF_Generator::generate_offer((int)$request['id'], 'download');
        } catch (\Exception $e) {
            return new WP_Error('pdf_error', $e->getMessage(), array('status' => 500));
        }
        exit;
    }

    /**
     * Derived status: a sent offer past its validity reads as 'expired' (presentational
     * only — the stored status stays 'sent', so no cron and no drift).
     */
    private static function effective_status($row) {
        if (($row['status'] ?? '') === 'sent' && !empty($row['valid_until'])) {
            if (strtotime($row['valid_until']) < strtotime(date('Y-m-d'))) {
                return 'expired';
            }
        }
        return $row['status'] ?? 'draft';
    }

    public static function get_offers($request) {
        global $wpdb;

        $offers_table = KleinBooks_Database::get_table_name('offers');
        $clients_table = KleinBooks_Database::get_table_name('clients');
        $invoices_table = KleinBooks_Database::get_table_name('invoices');

        $status    = $request->get_param('status');
        $client_id = $request->get_param('client_id');
        $year      = $request->get_param('year');
        $search    = $request->get_param('search');
        $orderby   = $request->get_param('orderby') ?: 'id';
        $order     = $request->get_param('order') ?: 'DESC';

        $allowed_orderby = array('id', 'date_issued', 'offer_number', 'total', 'status', 'valid_until', 'created_at');
        $orderby = in_array($orderby, $allowed_orderby) ? $orderby : 'id';
        $order = strtoupper($order) === 'ASC' ? 'ASC' : 'DESC';

        $where = array('1=1');
        $params = array();

        // 'expired' is derived, so filter it specially (sent + past validity).
        $filter_expired = ($status === 'expired');
        if ($status && !$filter_expired) {
            $where[] = 'o.status = %s';
            $params[] = $status;
        }
        if ($filter_expired) {
            $where[] = "o.status = 'sent' AND o.valid_until IS NOT NULL AND o.valid_until < CURDATE()";
        }
        if ($client_id) {
            $where[] = 'o.client_id = %d';
            $params[] = (int)$client_id;
        }
        if ($year) {
            $where[] = 'o.offer_year = %d';
            $params[] = (int)$year;
        }
        if ($search) {
            $like = '%' . $wpdb->esc_like($search) . '%';
            $where[] = '(o.offer_number LIKE %s OR c.company_name LIKE %s)';
            $params[] = $like;
            $params[] = $like;
        }

        $where_clause = implode(' AND ', $where);
        // Forward cross-link surface: when converted, also return the linked invoice number
        // so the list can render a clickable badge (J.2 — converted_invoice_id is already on o.*).
        $query = "SELECT o.*, c.company_name as client_name, c.email as client_email,
                  (SELECT invoice_number FROM $invoices_table WHERE id = o.converted_invoice_id) as converted_invoice_number
                  FROM $offers_table o
                  LEFT JOIN $clients_table c ON o.client_id = c.id
                  WHERE $where_clause
                  ORDER BY o.$orderby $order";
        if (!empty($params)) {
            $query = $wpdb->prepare($query, $params);
        }

        $offers = $wpdb->get_results($query, ARRAY_A);
        foreach ($offers as &$o) {
            $o['effective_status'] = self::effective_status($o);
        }
        unset($o);

        return rest_ensure_response($offers);
    }

    public static function get_offer($request) {
        global $wpdb;

        $id = (int)$request['id'];
        $offers_table = KleinBooks_Database::get_table_name('offers');
        $offer_items_table = KleinBooks_Database::get_table_name('offer_items');
        $clients_table = KleinBooks_Database::get_table_name('clients');
        $invoices_table = KleinBooks_Database::get_table_name('invoices');

        $offer = $wpdb->get_row($wpdb->prepare(
            "SELECT o.*, c.company_name as client_name, c.email as client_email,
                    c.address_line1 as client_address, c.city as client_city,
                    c.postal_code as client_postal_code, c.country as client_country
             FROM $offers_table o
             LEFT JOIN $clients_table c ON o.client_id = c.id
             WHERE o.id = %d",
            $id
        ), ARRAY_A);

        if (!$offer) {
            return new WP_Error('not_found', 'Offer not found', array('status' => 404));
        }

        $offer['items'] = $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM $offer_items_table WHERE offer_id = %d ORDER BY sort_order, id",
            $id
        ), ARRAY_A);

        $offer['effective_status'] = self::effective_status($offer);

        // Forward link: the invoice this offer became (if any).
        if (!empty($offer['converted_invoice_id'])) {
            $offer['converted_invoice_number'] = $wpdb->get_var($wpdb->prepare(
                "SELECT invoice_number FROM $invoices_table WHERE id = %d",
                (int)$offer['converted_invoice_id']
            ));
        }

        return rest_ensure_response($offer);
    }

    public static function create_offer($request) {
        global $wpdb;

        $offers_table = KleinBooks_Database::get_table_name('offers');
        $offer_items_table = KleinBooks_Database::get_table_name('offer_items');
        $data = $request->get_json_params();

        $client_id = (int)($data['client_id'] ?? 0);
        if (!$client_id) {
            return new WP_Error('missing_client', 'Client is required', array('status' => 400));
        }

        $date_issued = $data['date_issued'] ?? date('Y-m-d');
        $offer_year = (int)date('Y', strtotime($date_issued));

        // Offer number with yearly reset, mirroring invoice numbering. Configurable prefix.
        $offer_sequence = 1;
        if (empty($data['offer_number'])) {
            $max = $wpdb->get_var($wpdb->prepare(
                "SELECT MAX(offer_sequence) FROM $offers_table WHERE offer_year = %d",
                $offer_year
            ));
            $offer_sequence = ($max ? (int)$max : 0) + 1;
            $prefix = KleinBooks_Database::get_setting('offer_prefix', 'ANG-');
            $data['offer_number'] = $prefix . str_pad($offer_sequence, 5, '0', STR_PAD_LEFT);
        } elseif (preg_match('/(\d{5})/', $data['offer_number'], $m)) {
            $offer_sequence = (int)$m[1];
        }

        // Validity: explicit, else date_issued + configurable default (14d).
        $validity_days = (int)KleinBooks_Database::get_setting('offer_validity_days', 14);
        $valid_until = $data['valid_until'] ?? date('Y-m-d', strtotime($date_issued . " + $validity_days days"));

        $totals = self::calc_totals($data);

        $result = $wpdb->insert($offers_table, array(
            'offer_number' => sanitize_text_field($data['offer_number']),
            'client_id' => $client_id,
            'offer_year' => $offer_year,
            'offer_sequence' => $offer_sequence,
            'date_issued' => $date_issued,
            'valid_until' => $valid_until,
            'status' => sanitize_text_field($data['status'] ?? 'draft'),
            'subtotal' => $totals['subtotal'],
            'tax_rate' => $totals['tax_rate'],
            'tax_amount' => $totals['tax_amount'],
            'total' => $totals['total'],
            'notes' => sanitize_textarea_field($data['notes'] ?? ''),
            'performance_period' => sanitize_text_field($data['performance_period'] ?? ''),
        ));

        if ($result === false) {
            return new WP_Error('db_error', 'Failed to create offer', array('status' => 500));
        }

        $offer_id = $wpdb->insert_id;
        self::replace_items($offer_id, $data['items'] ?? array());

        return rest_ensure_response(array(
            'id' => $offer_id,
            'offer_number' => $data['offer_number'],
            'message' => 'Offer created successfully',
        ));
    }

    public static function update_offer($request) {
        global $wpdb;

        $id = (int)$request['id'];
        $offers_table = KleinBooks_Database::get_table_name('offers');
        $data = $request->get_json_params();

        $existing = $wpdb->get_row($wpdb->prepare(
            "SELECT status, converted_invoice_id FROM $offers_table WHERE id = %d",
            $id
        ));
        if (!$existing) {
            return new WP_Error('not_found', 'Offer not found', array('status' => 404));
        }
        // A converted offer is the frozen historical record of what was quoted — locked.
        if ($existing->converted_invoice_id !== null || $existing->status === 'converted') {
            return new WP_Error(
                'offer_converted',
                __('Umgewandelte Angebote können nicht bearbeitet werden.', 'kleinbooks'),
                array('status' => 403, 'code' => 'CONVERTED')
            );
        }
        // 'converted' is reachable only via the convert endpoint.
        if (isset($data['status']) && $data['status'] === 'converted') {
            return new WP_Error('use_convert_endpoint', __('Bitte die Umwandlungsfunktion verwenden.', 'kleinbooks'), array('status' => 400));
        }

        if (!empty($data['items'])) {
            $totals = self::calc_totals($data);
            $data['subtotal'] = $totals['subtotal'];
            $data['tax_amount'] = $totals['tax_amount'];
            $data['total'] = $totals['total'];
        }

        $update = array();
        $fields = array('client_id', 'date_issued', 'valid_until', 'status', 'subtotal', 'tax_rate', 'tax_amount', 'total', 'notes', 'performance_period');
        foreach ($fields as $f) {
            if (!isset($data[$f])) continue;
            if ($f === 'client_id') {
                $update[$f] = (int)$data[$f];
            } elseif (in_array($f, array('subtotal', 'tax_rate', 'tax_amount', 'total'))) {
                $update[$f] = (float)$data[$f];
            } elseif ($f === 'notes') {
                $update[$f] = sanitize_textarea_field($data[$f]);
            } else {
                $update[$f] = sanitize_text_field($data[$f]);
            }
        }

        if (!empty($update)) {
            if ($wpdb->update($offers_table, $update, array('id' => $id)) === false) {
                return new WP_Error('db_error', 'Failed to update offer', array('status' => 500));
            }
        }

        if (isset($data['items'])) {
            self::replace_items($id, $data['items']);
        }

        return rest_ensure_response(array('message' => 'Offer updated successfully'));
    }

    public static function delete_offer($request) {
        global $wpdb;

        $id = (int)$request['id'];
        $offers_table = KleinBooks_Database::get_table_name('offers');
        $offer_items_table = KleinBooks_Database::get_table_name('offer_items');

        $existing = $wpdb->get_row($wpdb->prepare(
            "SELECT converted_invoice_id FROM $offers_table WHERE id = %d",
            $id
        ));
        if (!$existing) {
            return new WP_Error('not_found', 'Offer not found', array('status' => 404));
        }
        // Don't orphan a converted invoice's source link.
        if ($existing->converted_invoice_id !== null) {
            return new WP_Error(
                'offer_converted',
                __('Umgewandelte Angebote können nicht gelöscht werden.', 'kleinbooks'),
                array('status' => 403, 'code' => 'CONVERTED')
            );
        }

        $wpdb->delete($offer_items_table, array('offer_id' => $id));
        $wpdb->delete($offers_table, array('id' => $id));

        return rest_ensure_response(array('message' => 'Offer deleted successfully'));
    }

    public static function update_offer_status($request) {
        global $wpdb;

        $id = (int)$request['id'];
        $offers_table = KleinBooks_Database::get_table_name('offers');
        $data = $request->get_json_params();
        $status = sanitize_text_field($data['status'] ?? '');

        if (!in_array($status, self::SETTABLE_STATUSES)) {
            return new WP_Error('invalid_status', 'Invalid status', array('status' => 400));
        }

        $existing = $wpdb->get_row($wpdb->prepare(
            "SELECT status, converted_invoice_id FROM $offers_table WHERE id = %d",
            $id
        ));
        if (!$existing) {
            return new WP_Error('not_found', 'Offer not found', array('status' => 404));
        }
        if ($existing->converted_invoice_id !== null || $existing->status === 'converted') {
            return new WP_Error('offer_converted', __('Status von umgewandelten Angeboten kann nicht geändert werden.', 'kleinbooks'), array('status' => 403));
        }

        // Stamp/clear the relevant timestamp so the audit trail reflects the transition.
        $update = array('status' => $status);
        $now = current_time('mysql');
        $update['accepted_at'] = $status === 'accepted' ? $now : null;
        $update['declined_at'] = $status === 'declined' ? $now : null;
        if ($status === 'sent' && $existing->status !== 'sent') {
            $update['sent_at'] = $now;
        }

        if ($wpdb->update($offers_table, $update, array('id' => $id)) === false) {
            return new WP_Error('db_error', 'Failed to update status', array('status' => 500));
        }

        return rest_ensure_response(array(
            'message' => 'Status updated successfully',
            'status' => $status,
            'accepted_at' => $update['accepted_at'],
            'declined_at' => $update['declined_at'],
        ));
    }

    public static function get_next_offer_number($request) {
        global $wpdb;

        $offers_table = KleinBooks_Database::get_table_name('offers');
        $year = (int)($request->get_param('year') ?: date('Y'));
        $max = $wpdb->get_var($wpdb->prepare(
            "SELECT MAX(offer_sequence) FROM $offers_table WHERE offer_year = %d",
            $year
        ));
        $sequence = ($max ? (int)$max : 0) + 1;
        $prefix = KleinBooks_Database::get_setting('offer_prefix', 'ANG-');

        return rest_ensure_response(array(
            'offer_number' => $prefix . str_pad($sequence, 5, '0', STR_PAD_LEFT),
            'sequence' => $sequence,
            'year' => $year,
        ));
    }

    /**
     * Convert an offer into a new DRAFT invoice. Copies client, line items, notes and
     * performance period; assigns a fresh invoice number; cross-links both records and
     * marks the offer 'converted' (terminal). Returns the new invoice id so the UI can
     * open its drawer pre-filled. The new invoice is a normal draft — fully editable.
     */
    public static function convert_to_invoice($request) {
        global $wpdb;

        $id = (int)$request['id'];
        $offers_table = KleinBooks_Database::get_table_name('offers');
        $offer_items_table = KleinBooks_Database::get_table_name('offer_items');
        $invoices_table = KleinBooks_Database::get_table_name('invoices');
        $invoice_items_table = KleinBooks_Database::get_table_name('invoice_items');
        $clients_table = KleinBooks_Database::get_table_name('clients');

        $offer = $wpdb->get_row($wpdb->prepare("SELECT * FROM $offers_table WHERE id = %d", $id), ARRAY_A);
        if (!$offer) {
            return new WP_Error('not_found', 'Offer not found', array('status' => 404));
        }

        // Already converted → don't create a duplicate; hand back the existing invoice.
        if (!empty($offer['converted_invoice_id'])) {
            return rest_ensure_response(array(
                'invoice_id' => (int)$offer['converted_invoice_id'],
                'already_converted' => true,
                'message' => 'Offer was already converted',
            ));
        }

        $client_id = (int)$offer['client_id'];
        $client_code = $wpdb->get_var($wpdb->prepare("SELECT client_code FROM $clients_table WHERE id = %d", $client_id)) ?: '';

        // New invoice number (yearly reset), mirroring create_invoice.
        $date_issued = date('Y-m-d');
        $invoice_year = (int)date('Y');
        $max_seq = $wpdb->get_var($wpdb->prepare(
            "SELECT MAX(invoice_sequence) FROM $invoices_table WHERE invoice_year = %d",
            $invoice_year
        ));
        $invoice_sequence = ($max_seq ? (int)$max_seq : 0) + 1;
        $prefix = KleinBooks_Database::get_setting('invoice_prefix', 'MK_');
        $invoice_number = $prefix . str_pad($invoice_sequence, 5, '0', STR_PAD_LEFT) . ($client_code !== '' ? '_' . $client_code : '');

        $payment_terms = (int)KleinBooks_Database::get_setting('payment_terms_days', 10);
        $date_due = date('Y-m-d', strtotime($date_issued . " + $payment_terms days"));

        $inserted = $wpdb->insert($invoices_table, array(
            'invoice_number' => $invoice_number,
            'client_id' => $client_id,
            'invoice_year' => $invoice_year,
            'invoice_sequence' => $invoice_sequence,
            'date_issued' => $date_issued,
            'date_due' => $date_due,
            'status' => 'draft',
            'subtotal' => (float)$offer['subtotal'],
            'tax_rate' => (float)$offer['tax_rate'],
            'tax_amount' => (float)$offer['tax_amount'],
            'total' => (float)$offer['total'],
            'notes' => $offer['notes'],
            'performance_period' => $offer['performance_period'],
        ));
        if ($inserted === false) {
            return new WP_Error('db_error', 'Failed to create invoice from offer', array('status' => 500));
        }
        $invoice_id = (int)$wpdb->insert_id;

        // Copy line items verbatim (the draft invoice is editable afterwards).
        $items = $wpdb->get_results($wpdb->prepare(
            "SELECT description, details, quantity, unit_price, total, sort_order
             FROM $offer_items_table WHERE offer_id = %d ORDER BY sort_order, id",
            $id
        ), ARRAY_A);
        foreach ($items as $item) {
            $wpdb->insert($invoice_items_table, array(
                'invoice_id' => $invoice_id,
                'description' => $item['description'],
                'details' => $item['details'],
                'quantity' => $item['quantity'],
                'unit_price' => $item['unit_price'],
                'total' => $item['total'],
                'sort_order' => $item['sort_order'],
            ));
        }

        // Atomically claim the offer — only the request whose UPDATE still sees a NULL link
        // wins, so a rapid double-convert can't leave two invoices. (Single-user app, but cheap.)
        $claimed = $wpdb->query($wpdb->prepare(
            "UPDATE $offers_table SET status = 'converted', converted_at = %s, converted_invoice_id = %d
             WHERE id = %d AND converted_invoice_id IS NULL",
            current_time('mysql'), $invoice_id, $id
        ));

        if (!$claimed) {
            // Lost the race (already converted in parallel). Roll back our orphan invoice +
            // items and hand back the invoice that actually won.
            $wpdb->delete($invoice_items_table, array('invoice_id' => $invoice_id));
            $wpdb->delete($invoices_table, array('id' => $invoice_id));
            $winner = (int)$wpdb->get_var($wpdb->prepare(
                "SELECT converted_invoice_id FROM $offers_table WHERE id = %d", $id
            ));
            return rest_ensure_response(array(
                'invoice_id' => $winner,
                'already_converted' => true,
                'message' => 'Offer was already converted',
            ));
        }

        return rest_ensure_response(array(
            'invoice_id' => $invoice_id,
            'invoice_number' => $invoice_number,
            'message' => 'Offer converted to invoice',
        ));
    }

    // ---- helpers ----

    private static function calc_totals($data) {
        $subtotal = 0;
        if (!empty($data['items'])) {
            foreach ($data['items'] as $item) {
                $subtotal += (float)($item['total'] ?? ((float)($item['quantity'] ?? 1) * (float)($item['unit_price'] ?? 0)));
            }
        }
        $tax_rate = (float)($data['tax_rate'] ?? 0);
        $tax_amount = $subtotal * ($tax_rate / 100);
        return array(
            'subtotal' => $subtotal,
            'tax_rate' => $tax_rate,
            'tax_amount' => $tax_amount,
            'total' => $subtotal + $tax_amount,
        );
    }

    private static function replace_items($offer_id, $items) {
        global $wpdb;
        $offer_items_table = KleinBooks_Database::get_table_name('offer_items');
        $wpdb->delete($offer_items_table, array('offer_id' => $offer_id));
        foreach ((array)$items as $index => $item) {
            $quantity = (float)($item['quantity'] ?? 1);
            $unit_price = (float)($item['unit_price'] ?? 0);
            $item_total = (float)($item['total'] ?? ($quantity * $unit_price));
            $wpdb->insert($offer_items_table, array(
                'offer_id' => $offer_id,
                'description' => sanitize_text_field($item['description'] ?? ''),
                'details' => sanitize_textarea_field($item['details'] ?? ''),
                'quantity' => $quantity,
                'unit_price' => $unit_price,
                'total' => $item_total,
                'sort_order' => $index,
            ));
        }
    }
}
