<?php
declare(strict_types=1);

namespace Instinctor\Services;

use Instinctor\Core\Config;
use Instinctor\Core\Tenant;

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

/**
 * Per-owner business profile — the single source of truth for a tenant's real-world
 * details (name, address, phone, hours, socials). Every InStudio site owner has one.
 *
 * It is a thin, typed facade over Config's owner-scoped store (instinctor_owner_config,
 * see Config::getForOwner/setForOwner). No new table: each field is one config key,
 * stored per owner_id with global fallback. SMS notifications, auto-SEO (LocalBusiness
 * schema + meta), the booking widget, and the tenant dashboard all read from HERE so
 * the owner fills it in ONCE.
 *
 * Design note (Sergei 2026-07-14): owners are NOT technical. Field labels/help are
 * written for a salon owner, not a developer. "notify_phone" is "text me when someone
 * books", not "SMS webhook target".
 *
 * @package Instinctor
 */
final class BusinessProfile
{
    /** Config-key prefix for profile fields (namespaced so they never collide). */
    private const KEY = 'instinctor_bp_';

    /**
     * Canonical field registry. type drives the settings-UI control + validation.
     * `label`/`help` are owner-facing (plain language). `maps_to` reuses an existing
     * global config key so legacy readers keep working during the transition.
     *
     * @return array<string,array{label:string,help:string,type:string,maps_to?:string,default?:mixed}>
     */
    public static function fields(): array
    {
        return [
            'business_name' => ['label' => 'Business name',        'help' => 'What your business is called.',                                 'type' => 'text',  'maps_to' => 'instinctor_store_name'],
            'tagline'       => ['label' => 'One-line description',  'help' => 'A short line under your name, e.g. "Fair prices, same-day service".', 'type' => 'text',  'maps_to' => 'instinctor_store_description'],
            // This field's help has promised "shown on your site" since it was written,
            // and nothing ever rendered it: a plumber's site carried no number at all,
            // so the emergency caller had a form and nothing to ring (Sergei 2026-07-31).
            // The bar above the built-with credit renders it now, gated on phone_public.
            'phone'         => ['label' => 'Phone number',         'help' => 'Shown on your site so customers can call you.',                  'type' => 'tel'],
            // NO maps_to on the public phone. publish-free stores the owner's MOBILE as
            // instinctor_store_phone (its lifecycle-reach number), and the legacy
            // fallback printed that private mobile at the foot of the published site:
            // the exact leak the notify_phone split was made to close (Sergei
            // 2026-08-01, seen as the publish card's number under Visit us). The public
            // number is opt-in from Settings and nothing else, ever.
            // DEFAULT ON. The number given at publish IS the business number, and for
            // a trade the call is the lead: somebody with water coming through a
            // ceiling rings, they do not fill in a form. An owner who wants it off
            // turns it off in Settings after publishing (Sergei 2026-08-01).
            'phone_public'  => ['label' => 'Show my number on the site', 'help' => 'A tap-to-call bar at the foot of every page. Turn it off to take enquiries through the form only.', 'type' => 'toggle', 'default' => '1'],
            'hero_3d'       => ['label' => '3D motion on photos', 'help' => 'The gentle drift on your background photos. Turn it off and they stand still.', 'type' => 'toggle', 'default' => '1'],
            'notify_phone'  => ['label' => 'Text me about new customers', 'help' => 'We text this number the moment someone books or asks for a quote. Usually your mobile.', 'type' => 'tel'],
            'email'         => ['label' => 'Contact email',        'help' => 'Where new-message and lead notifications are emailed.',          'type' => 'email', 'maps_to' => 'instinctor_store_email'],
            'address'       => ['label' => 'Street address',       'help' => 'Your business address. Leave blank if you work mobile or from home.', 'type' => 'text',  'maps_to' => 'instinctor_store_address'],
            'city'          => ['label' => 'City and State',       'help' => '',                                                              'type' => 'text',  'maps_to' => 'instinctor_store_city'],
            'postal_code'   => ['label' => 'ZIP / postal code',    'help' => '',                                                              'type' => 'text',  'maps_to' => 'instinctor_store_postcode'],
            'hours'         => ['label' => 'Opening hours',        'help' => 'Sets your site, your hours, and your Google listing at once.', 'type' => 'hours'],
            'price_range'   => ['label' => 'Price range',          'help' => 'Roughly how pricey you are, shown to Google.',                   'type' => 'select', 'default' => '$$'],
            'instagram_url' => ['label' => 'Instagram',            'help' => 'Your Instagram profile link.',                                  'type' => 'url'],
            'facebook_url'  => ['label' => 'Facebook',             'help' => 'Your Facebook page link.',                                      'type' => 'url'],
            // No "Connect Google" here: the button promised a GBP OAuth flow that does not
            // exist (removed 2026-07-17). Reviews are manual and complete without it:
            // /dashboard/reviews grows them (one-tap review ask) and shows them (paste the
            // best ones into the site). If GBP OAuth ever ships, it earns its row back.
            'google_maps_url'  => ['label' => 'Google Maps link',  'help' => 'A link to your listing on Google Maps, for the map pin on your site.', 'type' => 'url'],
        ];
    }

    /** Resolve the owner id: explicit arg, else the current Tenant context, else 0 (global). */
    private static function resolveOwner(?int $ownerId): int
    {
        if ($ownerId !== null && $ownerId > 0) return $ownerId;
        return Tenant::currentOwnerId() ?? 0;
    }

    /** Read one profile field for an owner (falls back to a mapped legacy key, then default). */
    /** The seven days, in the order a human reads a week. */
    public const HOURS_DAYS = [
        'mon' => 'Monday', 'tue' => 'Tuesday', 'wed' => 'Wednesday', 'thu' => 'Thursday',
        'fri' => 'Friday', 'sat' => 'Saturday', 'sun' => 'Sunday',
    ];

    /**
     * Read whatever the owner typed into the old free-text box.
     *
     * This is the load-bearing half of replacing a textarea with a grid: existing owners
     * have prose in there ("Tue-Fri 9am-7pm", "Sat 9am-5pm", "Sun-Mon closed") and a grid
     * that cannot read it would silently blank their real opening hours the first time
     * they saved anything else on the page.
     *
     * Best-effort by design: anything it cannot parse leaves that day untouched rather
     * than guessing, and the owner sees empty rows to fill rather than wrong ones to spot.
     *
     * @return array<string,array{open:bool,start:string,end:string}>
     */
    public static function hoursFrom(string $raw): array
    {
        $out = [];
        foreach (array_keys(self::HOURS_DAYS) as $d) {
            $out[$d] = ['open' => false, 'start' => '', 'end' => ''];
        }
        $raw = trim($raw);
        if ($raw === '') return $out;

        // Already structured (saved by this grid): JSON round-trips exactly.
        $json = json_decode($raw, true);
        if (is_array($json)) {
            foreach ($out as $d => $_) {
                if (!isset($json[$d]) || !is_array($json[$d])) continue;
                $out[$d] = [
                    'open'  => !empty($json[$d]['open']),
                    'start' => (string) ($json[$d]['start'] ?? ''),
                    'end'   => (string) ($json[$d]['end'] ?? ''),
                ];
            }
            return $out;
        }

        $keys  = array_keys(self::HOURS_DAYS);
        $idx   = array_flip($keys);
        $to24  = static function (string $t): string {
            $t = strtolower(str_replace([' ', '.'], '', trim($t)));
            if (!preg_match('/^(\d{1,2})(?::(\d{2}))?(am|pm)?$/', $t, $m)) return '';
            $h = (int) $m[1];
            $min = $m[2] ?? '00';
            $ap = $m[3] ?? '';
            if ($ap === 'pm' && $h < 12) $h += 12;
            if ($ap === 'am' && $h === 12) $h = 0;
            if ($h > 23) return '';
            return sprintf('%02d:%02d', $h, $min);
        };

        foreach (preg_split('/[\r\n,]+/', $raw) ?: [] as $line) {
            $line = trim($line);
            if ($line === '') continue;
            $low = strtolower($line);
            // Which days does this line name? A range ("tue-fri") or singles ("sat").
            $named = [];
            if (preg_match('/\b(mon|tue|wed|thu|fri|sat|sun)[a-z]*\s*[-\x{2013}\x{2014}to]+\s*(mon|tue|wed|thu|fri|sat|sun)[a-z]*/u', $low, $m)) {
                $a = $idx[$m[1]] ?? null; $b = $idx[$m[2]] ?? null;
                if ($a !== null && $b !== null) {
                    for ($i = $a; ; $i = ($i + 1) % 7) { $named[] = $keys[$i]; if ($i === $b) break; }
                }
            } else {
                foreach ($keys as $d) if (preg_match('/\b' . $d . '/', $low)) $named[] = $d;
            }
            if (!$named) continue;

            if (strpos($low, 'closed') !== false) {
                foreach ($named as $d) $out[$d] = ['open' => false, 'start' => '', 'end' => ''];
                continue;
            }
            if (preg_match('/(\d{1,2}(?::\d{2})?\s*(?:am|pm)?)\s*[-\x{2013}\x{2014}to]+\s*(\d{1,2}(?::\d{2})?\s*(?:am|pm)?)/u', $low, $t)) {
                $s = $to24($t[1]); $en = $to24($t[2]);
                if ($s !== '' && $en !== '') {
                    foreach ($named as $d) $out[$d] = ['open' => true, 'start' => $s, 'end' => $en];
                }
            }
        }
        return $out;
    }

    public static function get(string $field, ?int $ownerId = null): mixed
    {
        $ownerId = self::resolveOwner($ownerId);
        $def     = self::fields()[$field] ?? null;
        if ($def === null) return null;

        // OWNER-ONLY for a tenant. Every field here NAMES OR ADDRESSES A BUSINESS:
        // its name, tagline, phone, contact email, address. Config::getForOwner falls
        // back to the vendor's global when a tenant has not set a key, which is right
        // for a neutral default (currency) and a leak for all of these: a fresh tenant's
        // Settings showed OUR tagline ("A standalone web production platform..."), OUR
        // helpdesk@instinctor.com and OUR phone number as if they were theirs
        // (Sergei 2026-07-26). Config::ownerOnly exists for exactly this and was simply
        // not used here.
        //
        // It also silently broke the publish-time seeding: that only writes a field when
        // get() comes back blank, and the vendor fallback made every field look filled,
        // so nothing was ever seeded. One root cause, two symptoms - and the reason
        // 'email' and the footer brand each got a workaround instead of a fix.
        $read = static function (string $key) use ($ownerId) {
            return $ownerId > 0
                ? Config::ownerOnly($ownerId, $key, null)
                : Config::getForOwner($ownerId, $key, null);   // the vendor's own profile
        };
        $val = $read(self::KEY . $field);
        if (($val === null || $val === '') && !empty($def['maps_to'])) {
            $val = $read($def['maps_to']);                     // legacy key, same scoping
        }
        if ($val === null || $val === '') {
            $val = $def['default'] ?? '';                      // a real default ('$$'), never ours
        }
        return $val;
    }

    /** Write one profile field for an owner (also mirrors to the mapped legacy key). */
    public static function set(string $field, mixed $value, ?int $ownerId = null): bool
    {
        $ownerId = self::resolveOwner($ownerId);
        $def     = self::fields()[$field] ?? null;
        if ($def === null) return false;

        $ok = Config::setForOwner($ownerId, self::KEY . $field, $value);
        if (!empty($def['maps_to'])) {
            Config::setForOwner($ownerId, $def['maps_to'], $value);        // keep legacy readers in sync
        }
        return $ok;
    }

    /** The whole profile as an assoc array, for the settings UI / SEO / schema builders. */
    public static function all(?int $ownerId = null): array
    {
        $ownerId = self::resolveOwner($ownerId);
        $out = [];
        foreach (array_keys(self::fields()) as $field) {
            $out[$field] = self::get($field, $ownerId);
        }
        return $out;
    }

    /** True once the owner has entered the essentials (name + a way to be found). */
    public static function isUsable(?int $ownerId = null): bool
    {
        $ownerId = self::resolveOwner($ownerId);
        $name = trim((string) self::get('business_name', $ownerId));
        $loc  = trim((string) (self::get('address', $ownerId) ?: self::get('city', $ownerId)));
        return $name !== '' && $loc !== '';
    }
}
