<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Database;
use App\Models\SubscriptionCodeModel;
use RuntimeException;

/**
 * SubscriptionService
 *
 * The one place that creates a new tenant (a business_lines row) and
 * the one place that changes a tenant's subscription state — called
 * from the public signup flow, the Stripe webhook handler, and the
 * platform admin console alike, so "how does a company get access"
 * has a single answer regardless of which of those three triggered it.
 */
final class SubscriptionService
{
    public function __construct(
        private readonly SubscriptionCodeModel $codes = new SubscriptionCodeModel(),
    ) {
    }

    /**
     * Creates a brand-new tenant in a 'trialing' (unpaid) state, plus its
     * first user (who becomes that tenant's own admin). Activation
     * (Stripe checkout or a code) happens as a separate, following step
     * — this only reserves the subdomain and account shell.
     *
     * @return array{business_line_id:int, user_id:int}
     */
    public function registerTenant(
        string $companyName,
        string $subdomainSlug,
        string $ownerFirstName,
        string $ownerLastName,
        string $ownerEmail,
        string $ownerPasswordHash,
    ): array {
        $slug = $this->normalizeSlug($subdomainSlug);
        $db = Database::connection();

        $exists = $db->prepare('SELECT 1 FROM business_lines WHERE slug = :slug');
        $exists->execute(['slug' => $slug]);
        if ($exists->fetchColumn() !== false) {
            throw new RuntimeException("That subdomain is already taken: {$slug}");
        }

        $db->beginTransaction();

        try {
            $db->prepare(
                'INSERT INTO business_lines
                    (name, slug, subscription_status, billing_cycle, billing_email, trial_ends_at, created_at, updated_at)
                 VALUES (:name, :slug, \'trialing\', \'yearly\', :billing_email, DATE_ADD(NOW(), INTERVAL 14 DAY), NOW(), NOW())'
            )->execute(['name' => $companyName, 'slug' => $slug, 'billing_email' => $ownerEmail]);

            $businessLineId = (int) $db->lastInsertId();

            $roleId = (int) $db->query("SELECT id FROM roles WHERE slug = 'operations_admin' LIMIT 1")->fetchColumn();

            $db->prepare(
                'INSERT INTO users (role_id, first_name, last_name, email, password_hash, status, created_at, updated_at)
                 VALUES (:role_id, :first_name, :last_name, :email, :password_hash, \'active\', NOW(), NOW())'
            )->execute([
                'role_id' => $roleId ?: null,
                'first_name' => $ownerFirstName,
                'last_name' => $ownerLastName,
                'email' => $ownerEmail,
                'password_hash' => $ownerPasswordHash,
            ]);

            $userId = (int) $db->lastInsertId();

            $db->prepare('INSERT INTO user_business_lines (user_id, business_line_id) VALUES (:user_id, :bl)')
                ->execute(['user_id' => $userId, 'bl' => $businessLineId]);

            $this->logEvent($businessLineId, 'trial_started', "14-day trial for {$slug}", null);

            $db->commit();

            return ['business_line_id' => $businessLineId, 'user_id' => $userId];
        } catch (\Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }

    /** Called once a Stripe Checkout session completes successfully. */
    public function activateFromStripe(int $businessLineId, string $stripeCustomerId, string $stripeSubscriptionId): void
    {
        $this->setActiveYearly($businessLineId, 'signup_stripe', null);

        Database::connection()->prepare(
            'UPDATE business_lines SET stripe_customer_id = :cust, stripe_subscription_id = :sub WHERE id = :id'
        )->execute(['cust' => $stripeCustomerId, 'sub' => $stripeSubscriptionId, 'id' => $businessLineId]);

        $this->logEvent($businessLineId, 'checkout_completed', 'Activated via Stripe Checkout', null);
    }

    /** Called on Stripe's invoice.paid webhook for a renewal (existing subscription, new billing period). */
    public function renewFromStripe(int $businessLineId, string $stripeEventId): void
    {
        $this->setActiveYearly($businessLineId, null, null);
        $this->logEvent($businessLineId, 'renewed', 'Renewed via Stripe', $stripeEventId);
    }

    public function markPastDue(int $businessLineId, string $stripeEventId): void
    {
        Database::connection()->prepare(
            "UPDATE business_lines SET subscription_status = 'past_due' WHERE id = :id"
        )->execute(['id' => $businessLineId]);

        $this->logEvent($businessLineId, 'payment_failed', 'Stripe reported a failed payment', $stripeEventId);
    }

    /**
     * Redeems a subscription code for the given tenant. Handles both a
     * brand-new signup (intent = activate_new) and a renewal on an
     * already-existing tenant (intent = renew_existing) — the caller
     * decides which context it's in; this just enforces the code
     * itself allows that use.
     */
    public function redeemCode(string $plaintextCode, int $businessLineId, bool $isNewSignup): void
    {
        $code = $this->codes->findValidByPlaintext($plaintextCode);

        if ($code === null) {
            throw new RuntimeException('That code is invalid, expired, or already used.');
        }

        if ($code['business_line_id'] !== null && (int) $code['business_line_id'] !== $businessLineId) {
            throw new RuntimeException('That code is assigned to a different company.');
        }

        $wantedIntent = $isNewSignup ? 'activate_new' : 'renew_existing';
        if ($code['intent'] !== 'either' && $code['intent'] !== $wantedIntent) {
            throw new RuntimeException('That code cannot be used for this action.');
        }

        if (!$this->codes->redeem((int) $code['id'], $businessLineId)) {
            throw new RuntimeException('That code was just used by someone else — please request a new one.');
        }

        $this->setActiveYearly($businessLineId, 'signup_code', (int) $code['duration_months']);
        $this->logEvent($businessLineId, 'code_redeemed', "Redeemed code {$code['code_prefix']}...", null);
    }

    /** Platform admin onboarding a company by hand — no card, no code, just turn it on. */
    public function activateManually(int $businessLineId, int $platformAdminId, int $durationMonths = 12): void
    {
        $this->setActiveYearly($businessLineId, 'manual_admin', $durationMonths);

        Database::connection()->prepare(
            'UPDATE business_lines SET onboarded_by_admin_id = :admin WHERE id = :id'
        )->execute(['admin' => $platformAdminId, 'id' => $businessLineId]);

        $this->logEvent($businessLineId, 'manually_activated', 'Activated by platform admin', null);
    }

    public function cancel(int $businessLineId): void
    {
        Database::connection()->prepare(
            "UPDATE business_lines SET subscription_status = 'canceled' WHERE id = :id"
        )->execute(['id' => $businessLineId]);

        $this->logEvent($businessLineId, 'canceled', 'Subscription canceled', null);
    }

    private function setActiveYearly(int $businessLineId, ?string $activationSource, ?int $durationMonths): void
    {
        $months = $durationMonths ?? 12;

        $sql = "UPDATE business_lines
                SET subscription_status = 'active',
                    subscription_expires_at = DATE_ADD(NOW(), INTERVAL :months MONTH)";
        $params = ['months' => $months, 'id' => $businessLineId];

        if ($activationSource !== null) {
            $sql .= ', activation_source = :source';
            $params['source'] = $activationSource;
        }

        $sql .= ' WHERE id = :id';

        Database::connection()->prepare($sql)->execute($params);
    }

    private function logEvent(int $businessLineId, string $eventType, ?string $detail, ?string $stripeEventId): void
    {
        Database::connection()->prepare(
            'INSERT INTO subscription_events (business_line_id, event_type, detail, stripe_event_id, created_at)
             VALUES (:bl, :type, :detail, :stripe_id, NOW())'
        )->execute([
            'bl' => $businessLineId,
            'type' => $eventType,
            'detail' => $detail,
            'stripe_id' => $stripeEventId,
        ]);
    }

    private function normalizeSlug(string $raw): string
    {
        $slug = strtolower(trim($raw));
        $slug = preg_replace('/[^a-z0-9-]+/', '-', $slug) ?? '';
        $slug = trim($slug, '-');

        if ($slug === '' || strlen($slug) < 2) {
            throw new RuntimeException('Please choose a subdomain of at least 2 characters (letters, numbers, hyphens).');
        }

        return $slug;
    }
}
