<?php
/**
 * StoreProvisioner.php
 * ---------------------------------------------------------------------------
 * The engine behind "customer fills a form, gets a live store." Every step
 * is logged to `provisioning_log` in the master database as it happens, so
 * if something fails halfway through, you can see EXACTLY which step broke
 * instead of guessing — this matters a lot in practice, because a failed
 * database creation halfway through a run can otherwise leave an orphaned
 * subdomain or database with no obvious record of why.
 *
 * DESIGN: each step is its own method, and provision() runs them in order,
 * stopping and recording the failure point the moment anything throws.
 * Nothing here silently swallows an error — every catch either logs and
 * re-throws, or logs and does a best-effort cleanup before re-throwing.
 * ---------------------------------------------------------------------------
 */

require_once __DIR__ . '/CpanelApi.php';

class ProvisioningException extends \Exception {}

class StoreProvisioner
{
    private CpanelApi $cpanel;
    private PDO $masterDb;

    // ---- Configuration — fill these in for your real environment ----
    private string $rootDomain;        // "quickecommerce.co"
    private string $cpanelHomeDir;     // server path to your cPanel home, e.g. "/home/quickecom"
    private string $templatePath;      // clean QuickCatalog codebase to copy per store
    private string $storesSubdir;      // relative folder under public_html for stores, e.g. "stores"
    private string $baseSchemaSql;     // path to base-schema-raw.sql
    private string $featureAdditionsSql; // path to feature-additions.sql
    private string $defaultDataSql;    // path to default-data.sql (required rows)
    private string $configTemplate;    // path to config.template.ini

    public function __construct(
        CpanelApi $cpanel,
        PDO $masterDb,
        string $rootDomain,
        string $cpanelHomeDir,
        string $templatePath,
        string $storesSubdir = 'stores'
    ) {
        $this->cpanel        = $cpanel;
        $this->masterDb      = $masterDb;
        $this->rootDomain    = $rootDomain;
        $this->cpanelHomeDir = rtrim($cpanelHomeDir, '/');
        $this->templatePath  = rtrim($templatePath, '/');
        $this->storesSubdir  = trim($storesSubdir, '/');

        $sqlDir = dirname(__DIR__) . '/sql';
        $this->baseSchemaSql       = $sqlDir . '/base-schema-raw.sql';
        $this->featureAdditionsSql = $sqlDir . '/feature-additions.sql';
        $this->defaultDataSql      = $sqlDir . '/default-data.sql';
        $this->configTemplate      = __DIR__ . '/config.template.ini';
    }

    /**
     * Full provisioning run. $input keys required:
     *   store_name, subdomain, owner_name, owner_email, owner_phone (optional),
     *   admin_password, business_type (optional — used only for logging/seed choice)
     *
     * Returns an array with the new store's URL and admin login details on
     * success. Throws ProvisioningException on any failure, with the log
     * already recorded in provisioning_log for that subdomain.
     */
    public function provision(array $input): array
    {
        $subdomain = $this->validateSubdomain($input['subdomain']);
        $this->assertSubdomainAvailable($subdomain);

        $storeId = $this->createStoreRecord($input, $subdomain);

        try {
            $docRoot   = $this->createSubdomainAndFolder($subdomain, $storeId);
            [$dbName, $dbUser, $dbPass] = $this->createDatabase($subdomain, $storeId);
            $this->importSchema($dbName, $dbUser, $dbPass, $storeId);
            $this->copyCodebase($docRoot, $storeId);
            $this->writeConfig($docRoot, $dbName, $dbUser, $dbPass, $storeId);
            $adminUsername = $this->createAdminUser($dbName, $dbUser, $dbPass, $input, $storeId);
            $this->seedBasicSettings($dbName, $dbUser, $dbPass, $input, $storeId);
            $this->finalizeStoreRecord($storeId, $dbName, $dbUser, $docRoot, $adminUsername);

            return [
                'success'        => true,
                'store_url'      => "https://{$subdomain}.{$this->rootDomain}",
                'admin_url'      => "https://{$subdomain}.{$this->rootDomain}/admin",
                'admin_username' => $adminUsername,
            ];
        } catch (\Throwable $e) {
            $this->log($storeId, $subdomain, 'provision_failed', 'failed', $e->getMessage());
            $this->markStoreFailed($storeId);
            throw new ProvisioningException(
                "Store creation failed at some step — see provisioning_log for store id $storeId. " .
                $e->getMessage()
            );
        }
    }

    // ------------------------------------------------------------------
    // Validation
    // ------------------------------------------------------------------

    private function validateSubdomain(string $raw): string
    {
        $s = strtolower(trim($raw));
        $s = preg_replace('/[^a-z0-9\-]/', '', $s);
        $s = trim($s, '-');

        if (strlen($s) < 3) {
            throw new ProvisioningException('Store address must be at least 3 characters.');
        }
        if (strlen($s) > 40) {
            throw new ProvisioningException('Store address is too long (max 40 characters).');
        }
        return $s;
    }

    private function assertSubdomainAvailable(string $subdomain): void
    {
        $reserved = $this->masterDb->prepare('SELECT 1 FROM reserved_subdomains WHERE subdomain = ?');
        $reserved->execute([$subdomain]);
        if ($reserved->fetchColumn()) {
            throw new ProvisioningException("\"$subdomain\" is a reserved name — please choose another.");
        }

        $taken = $this->masterDb->prepare(
            "SELECT 1 FROM stores WHERE subdomain = ? AND status != 'deleted'"
        );
        $taken->execute([$subdomain]);
        if ($taken->fetchColumn()) {
            throw new ProvisioningException("\"$subdomain\" is already taken — please choose another.");
        }
    }

    /** Public helper for a live "is this available?" check from the signup form (AJAX). */
    public function isSubdomainAvailable(string $subdomain): bool
    {
        try {
            $this->assertSubdomainAvailable($this->validateSubdomain($subdomain));
            return true;
        } catch (\Throwable $e) {
            return false;
        }
    }

    // ------------------------------------------------------------------
    // Step 0: create the tracking row FIRST, in 'provisioning' status —
    // this guarantees every attempt is recorded, even ones that fail
    // on the very first real step.
    // ------------------------------------------------------------------

    private function createStoreRecord(array $input, string $subdomain): int
    {
        $stmt = $this->masterDb->prepare(
            'INSERT INTO stores
                (store_name, subdomain, full_url, owner_name, owner_email, owner_phone,
                 db_name, db_user, doc_root, business_type, admin_username, status, plan, trial_ends_at)
             VALUES (?, ?, ?, ?, ?, ?, "", "", "", ?, "", "provisioning", "demo", DATE_ADD(NOW(), INTERVAL 14 DAY))'
        );
        $stmt->execute([
            $input['store_name'],
            $subdomain,
            "https://{$subdomain}.{$this->rootDomain}",
            $input['owner_name'],
            $input['owner_email'],
            $input['owner_phone'] ?? null,
            $input['business_type'] ?? null,
        ]);
        $storeId = (int)$this->masterDb->lastInsertId();
        $this->log($storeId, $subdomain, 'create_store_record', 'ok', "store id $storeId");
        return $storeId;
    }

    // ------------------------------------------------------------------
    // Step 1: subdomain + folder
    // ------------------------------------------------------------------

    private function createSubdomainAndFolder(string $subdomain, int $storeId): string
    {
        $relativeDir = "public_html/{$this->storesSubdir}/{$subdomain}";
        try {
            $this->cpanel->createSubdomain($subdomain, $this->rootDomain, $relativeDir);
            $docRoot = "{$this->cpanelHomeDir}/{$relativeDir}";
            $this->log($storeId, $subdomain, 'create_subdomain', 'ok', $docRoot);
            return $docRoot;
        } catch (\Throwable $e) {
            $this->log($storeId, $subdomain, 'create_subdomain', 'failed', $e->getMessage());
            throw $e;
        }
    }

    // ------------------------------------------------------------------
    // Step 2: database + user + privileges
    // ------------------------------------------------------------------

    private function createDatabase(string $subdomain, int $storeId): array
    {
        // Short, DB-safe base derived from the subdomain (already sanitized).
        $short = substr(str_replace('-', '', $subdomain), 0, 16);
        $dbPass = $this->generateStrongPassword();

        try {
            $dbName = $this->cpanel->createDatabase($short);
            $dbUser = $this->cpanel->createDbUser($short, $dbPass);
            $this->cpanel->grantAllPrivileges($dbUser, $dbName);
            $this->log($storeId, $subdomain, 'create_database', 'ok', "$dbName / $dbUser");
            return [$dbName, $dbUser, $dbPass];
        } catch (\Throwable $e) {
            $this->log($storeId, $subdomain, 'create_database', 'failed', $e->getMessage());
            throw $e;
        }
    }

    private function generateStrongPassword(int $length = 20): string
    {
        $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#%^&*';
        $out = '';
        for ($i = 0; $i < $length; $i++) {
            $out .= $chars[random_int(0, strlen($chars) - 1)];
        }
        return $out;
    }

    // ------------------------------------------------------------------
    // Step 3: import the gold-master schema into the new database
    // ------------------------------------------------------------------

    private function importSchema(string $dbName, string $dbUser, string $dbPass, int $storeId): void
    {
        try {
            $pdo = $this->connectStoreDb($dbName, $dbUser, $dbPass);
            $this->runSqlFile($pdo, $this->baseSchemaSql);
            $this->runSqlFile($pdo, $this->featureAdditionsSql);
            $this->runSqlFile($pdo, $this->defaultDataSql);
            $this->log($storeId, '', 'import_schema', 'ok', 'base + feature-additions + default-data applied');
        } catch (\Throwable $e) {
            $this->log($storeId, '', 'import_schema', 'failed', $e->getMessage());
            throw $e;
        }
    }

    protected function connectStoreDb(string $dbName, string $dbUser, string $dbPass): PDO
    {
        return new PDO(
            "mysql:host=127.0.0.1;port=3306;dbname={$dbName};charset=utf8mb4",
            $dbUser,
            $dbPass,
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
        );
    }

    /** Runs a .sql file's statements one at a time (quote-aware split). */
    private function runSqlFile(PDO $pdo, string $path): void
    {
        if (!is_file($path)) {
            throw new ProvisioningException("SQL file not found: $path");
        }
        $sql = file_get_contents($path);
        foreach ($this->splitSqlStatements($sql) as $stmt) {
            $stmt = trim($stmt);
            if ($stmt === '') continue;
            $pdo->exec($stmt);
        }
    }

    /**
     * Quote-aware statement splitter that ALSO strips "-- line comment"
     * text as it tokenizes (outside of quoted strings), rather than only
     * checking whether an already-split chunk happens to START with a
     * comment. That naive check is unsafe: if a descriptive comment sits
     * directly above a real statement with no semicolon between them —
     * exactly how base-schema-raw.sql is formatted — the comment and the
     * following real SQL get glued into one chunk by the splitter, and a
     * "skip if it starts with --" check silently discards BOTH, including
     * the real ALTER/CREATE statement. This bug briefly caused every
     * PRIMARY KEY / AUTO_INCREMENT ALTER preceded by a comment to be
     * dropped without any error — confirmed and fixed during testing.
     */
    private function splitSqlStatements(string $sql): array
    {
        $stmts = []; $buf = ''; $inStr = false; $strChar = '';
        $len = strlen($sql);
        for ($i = 0; $i < $len; $i++) {
            $ch = $sql[$i];
            if ($inStr) {
                if ($ch === '\\') { $buf .= $ch . ($sql[$i + 1] ?? ''); $i++; continue; }
                if ($ch === $strChar) { $inStr = false; }
                $buf .= $ch;
            } else {
                // Strip a "-- ..." line comment up to (not including) the
                // newline, so it never ends up glued to real SQL.
                if ($ch === '-' && ($sql[$i + 1] ?? '') === '-' &&
                    (($sql[$i + 2] ?? ' ') === ' ' || ($sql[$i + 2] ?? '') === "\t" || ($sql[$i + 2] ?? '') === "\n")) {
                    $nl = strpos($sql, "\n", $i);
                    $i = ($nl === false) ? $len : $nl;
                    continue;
                }
                if ($ch === "'" || $ch === '"') { $inStr = true; $strChar = $ch; $buf .= $ch; }
                elseif ($ch === ';') { $stmts[] = $buf; $buf = ''; }
                else { $buf .= $ch; }
            }
        }
        if (trim($buf) !== '') $stmts[] = $buf;
        return $stmts;
    }

    // ------------------------------------------------------------------
    // Step 4: copy the codebase from the clean template
    // ------------------------------------------------------------------

    private function copyCodebase(string $docRoot, int $storeId): void
    {
        if (!is_dir($this->templatePath)) {
            throw new ProvisioningException("Template codebase not found at {$this->templatePath}");
        }
        try {
            $this->recursiveCopy($this->templatePath, $docRoot, [
                'config.ini', '.git', 'tmp', 'uploads', 'error_log',
            ]);
            // Fresh writable directories every store needs of its own
            foreach (['tmp', 'uploads', 'uploads/setting', 'uploads/products', 'uploads/brands'] as $d) {
                @mkdir("$docRoot/$d", 0755, true);
            }
            $this->log($storeId, '', 'copy_codebase', 'ok', "copied to $docRoot");
        } catch (\Throwable $e) {
            $this->log($storeId, '', 'copy_codebase', 'failed', $e->getMessage());
            throw $e;
        }
    }

    private function recursiveCopy(string $src, string $dst, array $exclude = []): void
    {
        if (!is_dir($dst)) mkdir($dst, 0755, true);
        $items = scandir($src);
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') continue;
            if (in_array($item, $exclude, true)) continue;

            $srcPath = "$src/$item";
            $dstPath = "$dst/$item";
            if (is_dir($srcPath)) {
                $this->recursiveCopy($srcPath, $dstPath, $exclude);
            } else {
                copy($srcPath, $dstPath);
            }
        }
    }

    // ------------------------------------------------------------------
    // Step 5: write this store's own config.ini from the template
    // ------------------------------------------------------------------

    private function writeConfig(string $docRoot, string $dbName, string $dbUser, string $dbPass, int $storeId): void
    {
        try {
            $tpl = file_get_contents($this->configTemplate);
            $tpl = str_replace(
                ['{{DB_NAME}}', '{{DB_USER}}', '{{DB_PASSWORD}}'],
                [$dbName, $dbUser, $dbPass],
                $tpl
            );
            file_put_contents("$docRoot/config.ini", $tpl);
            @chmod("$docRoot/config.ini", 0640); // owner read/write, group read, no world access
            $this->log($storeId, '', 'write_config', 'ok', 'config.ini written');
        } catch (\Throwable $e) {
            $this->log($storeId, '', 'write_config', 'failed', $e->getMessage());
            throw $e;
        }
    }

    // ------------------------------------------------------------------
    // Step 6: create the store owner's admin login
    // ------------------------------------------------------------------

    private function createAdminUser(string $dbName, string $dbUser, string $dbPass, array $input, int $storeId): string
    {
        try {
            $pdo = $this->connectStoreDb($dbName, $dbUser, $dbPass);
            $username = $this->slugifyUsername($input['owner_email']);
            $hash = password_hash($input['admin_password'], PASSWORD_BCRYPT);

            $stmt = $pdo->prepare(
                'INSERT INTO user_tbl (name, username, email, profile_pic, password, role, created_at)
                 VALUES (?, ?, ?, ?, ?, ?, NOW())'
            );
            $stmt->execute([$input['owner_name'], $username, $input['owner_email'], '', $hash, 'client']);

            $this->log($storeId, '', 'create_admin_user', 'ok', "username: $username");
            return $username;
        } catch (\Throwable $e) {
            $this->log($storeId, '', 'create_admin_user', 'failed', $e->getMessage());
            throw $e;
        }
    }

    private function slugifyUsername(string $email): string
    {
        $local = explode('@', $email)[0];
        return strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $local)) ?: 'admin';
    }

    // ------------------------------------------------------------------
    // Step 7: seed the setting row with the store's own name/branding
    // ------------------------------------------------------------------

    private function seedBasicSettings(string $dbName, string $dbUser, string $dbPass, array $input, int $storeId): void
    {
        try {
            $pdo = $this->connectStoreDb($dbName, $dbUser, $dbPass);
            $stmt = $pdo->prepare(
                'UPDATE setting SET brand_name = ?, contact_email = ? WHERE id = 1'
            );
            // `setting` needs exactly one row (id=1) to exist already from the
            // schema import for this UPDATE to have any effect — if your
            // base-schema-raw.sql doesn't seed that row, switch this to an
            // INSERT ... ON DUPLICATE KEY UPDATE instead.
            $stmt->execute([$input['store_name'], $input['owner_email']]);
            $this->log($storeId, '', 'seed_settings', 'ok', 'brand_name + contact_email set');
        } catch (\Throwable $e) {
            // Non-fatal — a store with default/blank settings still works,
            // the admin can fill these in from Site Settings themselves.
            $this->log($storeId, '', 'seed_settings', 'failed', $e->getMessage() . ' (non-fatal, continuing)');
        }
    }

    // ------------------------------------------------------------------
    // Step 8: mark the store row as active with its final details
    // ------------------------------------------------------------------

    private function finalizeStoreRecord(int $storeId, string $dbName, string $dbUser, string $docRoot, string $adminUsername): void
    {
        $stmt = $this->masterDb->prepare(
            "UPDATE stores SET db_name=?, db_user=?, doc_root=?, admin_username=?, status='active' WHERE id=?"
        );
        $stmt->execute([$dbName, $dbUser, $docRoot, $adminUsername, $storeId]);
    }

    private function markStoreFailed(int $storeId): void
    {
        $stmt = $this->masterDb->prepare("UPDATE stores SET status='expired' WHERE id=? AND status='provisioning'");
        // 'expired' is reused here just as a visible "did not complete"
        // marker distinct from a healthy demo — rename in your own admin
        // dashboard if you'd rather show a dedicated "failed" label.
        $stmt->execute([$storeId]);
    }

    // ------------------------------------------------------------------
    // Logging
    // ------------------------------------------------------------------

    private function log(?int $storeId, string $subdomain, string $step, string $status, ?string $message = null): void
    {
        $stmt = $this->masterDb->prepare(
            'INSERT INTO provisioning_log (store_id, subdomain, step, status, message) VALUES (?, ?, ?, ?, ?)'
        );
        $stmt->execute([$storeId, $subdomain, $step, $status, $message]);
    }
}
