<?php

namespace App\Service;

use App\Entity\Bible;
use App\Entity\Note;
use App\Entity\Reference;
use App\Entity\Series;
use App\Entity\Speaker;
use App\Entity\Template;
use App\Entity\User;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Persistence\ManagerRegistry;
use PDO;
use PDOException;
use Symfony\Component\Uid\Uuid;

class DatabaseTransferService
{
    private PDO $srcDB;
    private PDO $destDB;

    public function __construct(
        private string $projectDir,
        private ManagerRegistry $doctrine
    ) {
        // connect the source database
        switch (explode(":", $_ENV['DATABASE_URL'])[0]) {
            case 'pdo-sqlite':
                list($driverName, $dbFile) = explode(":/", $_ENV['DATABASE_URL']);
                $this->srcDB = new PDO('sqlite:'.$dbFile);
                break;
            case 'pdo-mysql':
            case 'pdo-pgsql':
                $dsn = $this->convertDBURLString($_ENV['DATABASE_URL']);
                $this->srcDB = new PDO($dsn['dsn'], $dsn['username'], $dsn['password']);
        }

        // connect the destination database
        switch(explode(':', $_ENV['XFER_DATABASE_URL'])[0]) {
            case 'pdo-sqlite':
                list($driverName, $dbFile) = explode(":/", $_ENV['XFER_DATABASE']);
                $this->destDB = new PDO('sqlite:'.$dbFile);
                break;
            case 'pdo-mysql':
            case 'pdo-pgsql':
                $dsn = $this->convertDBURLString($_ENV['XFER_DATABASE_URL']);
                $this->destDB = new PDO($dsn['dsn'], $dsn['username'], $dsn['password']);
        }
    }

    public function createSchema(): void
    {
        $destEm = $this->doctrine->getManager('transfer');
        $metadata = $destEm->getMetadataFactory()->getAllMetadata();
        $tool = new SchemaTool($destEm);
        
        $tool->dropSchema($metadata);
        $tool->createSchema($metadata);
    }

    public function getEntityClasses(): array
    {
        return [
            User::class,
            Bible::class,
            Reference::class,
            Template::class,
            Series::class,
            Speaker::class,
            Note::class
        ];
    }

    public function transferUserTable(): int
    {
        $sql = "SELECT * FROM user";
        $stmt = $this->srcDB->prepare($sql);
        $stmt->execute();
        $insQuery = "INSERT INTO user (id, email, roles, password, name, meta_data, home_church_rss) ".
            "VALUES ".
            "(:id, :email, :roles, :password, :name, :meta_data, :home_church)";
        $destStmt = $this->destDB->prepare($insQuery);
        $skippedCount = 0;

        $this->destDB->beginTransaction();
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $params = [
                'id' => $row['id'],
                'email' => $row['email'],
                'roles' => $row['roles'],
                'password' => $row['password'],
                'name' => $row['name'],
                'meta_data' => $row['meta_data'],
                'home_church' => $row['home_church_rss'],
            ];

            $this->destDB->exec("SAVEPOINT row_insert");

            try {
                $destStmt->execute($params);

                $this->destDB->exec("RELEASE SAVEPOINT row_insert");
            } catch (PDOException $e) {
                $skipCodes = [
                    '23000',
                    '23503',
                    '23505',
                ];
                if (in_array($e->getCode(), $skipCodes, true)) {
                    $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
                    $skippedCount++; // 4. Increment the counter
                    continue;
                }
                
                $this->destDB->rollBack();
                print_r($params);
                die("Critical Error: ".$e->getMessage());
            }
        }

        $this->destDB->commit();

        return $skippedCount;
    }

    public function transferBibleTable(): int
    {
        $sql = "SELECT * FROM bible";
        $stmt = $this->srcDB->prepare($sql);
        $stmt->execute();
        $insQuery = "INSERT INTO bible (id, book, chapter, verse, content, book_index, label) ".
            "VALUES ".
            "(:id, :book, :chapter, :verse, :content, :book_index, :label)";
        $destStmt = $this->destDB->prepare($insQuery);
        $skippedCount = 0;

        $this->destDB->beginTransaction();
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $uuid = Uuid::fromString($row['id']);

            $params = [
                'id' => $uuid->toBinary(),
                'book' => $row['book'],
                'chapter' => $row['chapter'],
                'verse' => $row['verse'],
                'content' => $row['content'],
                'book_index' => $row['book_index'],
                'label' => $row['label'],
            ];

            $this->destDB->exec("SAVEPOINT row_insert");

            try {
                $destStmt->execute($params);

                $this->destDB->exec("RELEASE SAVEPOINT row_insert");
            } catch (PDOException $e) {
                $skipCodes = [
                    '23000',
                    '23503',
                    '23505',
                ];
                if (in_array($e->getCode(), $skipCodes, true)) {
                    $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
                    $skippedCount++; // 4. Increment the counter
                    continue;
                }
                
                $this->destDB->rollBack();
                print_r($params);
                die("Critical Error: ".$e->getMessage());
            }
        } 
        $this->destDB->commit();

        return $skippedCount;
    }

    public function transferReferenceTable(): int
    {
        $sql = "SELECT * FROM reference";
        $stmt = $this->srcDB->prepare($sql);
        $stmt->execute();
        $insQuery = "INSERT INTO reference (id, type, name, label, ndx, content) ".
            "VALUES ".
            "(:id, :type, :name, :label, :ndx, :content)";
        $destStmt = $this->destDB->prepare($insQuery);
        $skippedCount = 0;
        
        $this->destDB->beginTransaction();
        while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $params = [
                'id' => $row['id'],
                'type' => $row['type'],
                'name' => $row['name'],
                'label' => $row['label'],
                'ndx' => $row['ndx'],
                'content' => $row['content'],
            ];

            $this->destDB->exec("SAVEPOINT row_insert");

            try {
                $destStmt->execute($params);

                $this->destDB->exec("RELEASE SAVEPOINT row_insert");
            } catch (PDOException $e) {
                $skipCodes = [
                    '23000',
                    '23503',
                    '23505',
                ];
                if (in_array($e->getCode(), $skipCodes, true)) {
                    $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
                    $skippedCount++; // 4. Increment the counter
                    continue;
                }
                
                $this->destDB->rollBack();
                print_r($params);
                die("Critical Error: ".$e->getMessage());
            }
        }

        $this->destDB->commit();

        return $skippedCount;
    }

    public function transferTemplateTable(): int
    {
        $sql = "SELECT * FROM template";
        $stmt = $this->srcDB->prepare($sql);
        $stmt->execute();
        $insQuery = "INSERT INTO template (id, user_id, name, content) ".
            "VALUES ".
            "(:id, :user_id, :name, :content)";
        $destStmt = $this->destDB->prepare($insQuery);
        $skippedCount = 0;

        $this->destDB->beginTransaction();
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $params = [
                'id' => $row['id'],
                'user_id' => $row['user_id'],
                'name' => $row['name'],
                'content' => $row['content'],
            ];

            $this->destDB->exec("SAVEPOINT row_insert");

            try {
                $destStmt->execute($params);

                $this->destDB->exec("RELEASE SAVEPOINT row_insert");
            } catch (PDOException $e) {
                $skipCodes = [
                    '23000',
                    '23503',
                    '23505',
                ];
                if (in_array($e->getCode(), $skipCodes, true)) {
                    $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
                    $skippedCount++; // 4. Increment the counter
                    continue;
                }
                
                $this->destDB->rollBack();
                print_r($params);
                die("Critical Error: ".$e->getMessage());
            }
        }
        
        $this->destDB->commit();

        return $skippedCount;
    }

    public function transferSpeakerTable(): int
    {
        $sql = "SELECT * FROM speaker";
        $stmt = $this->srcDB->prepare($sql);
        $stmt->execute();
        $insQuery = "INSERT INTO speaker (id, name, user_id) ".
            "VALUES ".
            "(:id, :name, :user_id)";
        $destStmt = $this->destDB->prepare($insQuery);
        $skippedCount = 0;

        $this->destDB->beginTransaction();
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $params = [
                'id' => $row['id'],
                'name' => $row['name'],
                'user_id' => $row['user_id'],
            ];

            $this->destDB->exec("SAVEPOINT row_insert");

            try {
                $destStmt->execute($params);

                $this->destDB->exec("RELEASE SAVEPOINT row_insert");
            } catch (PDOException $e) {
                $skipCodes = [
                    '23000',
                    '23503',
                    '23505',
                ];
                if (in_array($e->getCode(), $skipCodes, true)) {
                    $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
                    $skippedCount++; // 4. Increment the counter
                    continue;
                }
                
                $this->destDB->rollBack();
                print_r($params);
                die("Critical Error: ".$e->getMessage());
            }
        }

        $this->destDB->commit();

        return $skippedCount;
    }

    public function transferSeriesTable(): int
    {
        $sql = "SELECT * FROM series";
        $stmt = $this->srcDB->prepare($sql);
        $stmt->execute();
        $insQuery = "INSERT INTO series (id, name, user_id, template_id) ".
            "VALUES ".
            "(:id, :name, :user_id, :template_id)";
        $destStmt = $this->destDB->prepare($insQuery);
        $skippedCount = 0;

        $this->destDB->beginTransaction();
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $params = [
                'id' => $row['id'],
                'name' => $row['name'],
                'user_id' => $row['user_id'],
                'template_id' => $row['template_id'],
            ];

            $this->destDB->exec("SAVEPOINT row_insert");

            try {
                $destStmt->execute($params);

                $this->destDB->exec("RELEASE SAVEPOINT row_insert");
            } catch (PDOException $e) {
                $skipCodes = [
                    '23000',
                    '23503',
                    '23505',
                ];
                if (in_array($e->getCode(), $skipCodes, true)) {
                    $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
                    $skippedCount++; // 4. Increment the counter
                    continue;
                }
                
                $this->destDB->rollBack();
                print_r($params);
                die("Critical Error: ".$e->getMessage());
            }
        }

        $this->destDB->commit();

        return $skippedCount;
    }

    public function transferNoteTable(): int
    {
        $sql = "SELECT * FROM note";
        $stmt = $this->srcDB->prepare($sql);
        $stmt->execute();
        $insQuery = "INSERT INTO note (id, title, date, passage, refs, text, speaker_id, series_id, user_id, recording) ".
            "VALUES ".
            "(:id, :title, :date, :passage, :refs, :text, :speaker_id, :series_id, :user_id, :recording)";
        $destStmt = $this->destDB->prepare($insQuery);
        $skippedCount = 0;

        $this->destDB->beginTransaction();
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $params = [
                'id' => $row['id'],
                'speaker_id' => $row['speaker_id'],
                'series_id' => $row['series_id'],
                'user_id' => $row['user_id'],
                'title' => $row['title'],
                'date' => $row['date'],
                'passage' => $row['passage'],
                'refs' => $row['refs'],
                'text' => $row['text'],
                'recording' => $row['recording'],
            ];

            $this->destDB->exec("SAVEPOINT row_insert");

            try {
                $destStmt->execute($params);

                $this->destDB->exec("RELEASE SAVEPOINT row_insert");
            } catch (PDOException $e) {
                $skipCodes = [
                    '23000',
                    '23503',
                    '23505',
                ];
                if (in_array($e->getCode(), $skipCodes, true)) {
                    $this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
                    $skippedCount++; // 4. Increment the counter
                    continue;
                }
                
                $this->destDB->rollBack();
                print_r($params);
                die("Critical Error: ".$e->getMessage());
            }
        }

        $this->destDB->commit();

        return $skippedCount;
    }

    public function finalizeEnvSwap(): void
    {
        $envPath = $this->projectDir . '/.env';
        
        if (!file_exists($envPath)) {
            throw new \Exception(".env file not found at $envPath");
        }

        $lines = file($envPath);
        $newLines = [];
        $xferUrlValue = null;

        // 1. First pass: Find the value of XFER_DATABASE_URL
        foreach ($lines as $line) {
            if (preg_match('/^XFER_DATABASE_URL=(.*)/', trim($line), $matches)) {
                $xferUrlValue = $matches[1];
                break;
            }
        }

        if (!$xferUrlValue) {
            throw new \Exception("XFER_DATABASE_URL not found in .env file.");
        }

        // 2. Second pass: Perform the swap
        foreach ($lines as $line) {
            $trimmed = trim($line);

            // Prepend #OLD_ to the existing DATABASE_URL
            if (preg_match('/^DATABASE_URL=/', $trimmed)) {
                $newLines[] = "# OLD_" . $line;
                continue;
            }

            // Rename XFER_DATABASE_URL to DATABASE_URL
            if (preg_match('/^XFER_DATABASE_URL=/', $trimmed)) {
                $newLines[] = "DATABASE_URL=" . $xferUrlValue . PHP_EOL;
                continue;
            }

            $newLines[] = $line;
        }

        file_put_contents($envPath, implode('', $newLines));
    }

    public static function isTransferEnabled(): bool
    {
        return (isset($_ENV['XFER_DATABASE_URL']) && !empty($_ENV['XFER_DATABASE_URL']));
    }

    private function convertDBURLString(string $DBURL): array
    {
        $dsn = preg_split("/[:|\/|\@]+/", $DBURL);
        if (count($dsn) > 6) {
            die('You cannot have special characters in your password for the database entry during this process. Change the database user password to just use alpha-numeric characters, then run again. You can change it back to more secure after this transfer is complete');
        }
        
        $res = [
            'dsn' => substr($dsn[0], 4).':'.
                'host='.$dsn[3].';'.
                'port='.$dsn[4].';'.
                'dbname='.$dsn[5],
            'username' => $dsn[1],
            'password' => $dsn[2]
        ];

        return $res;
    }
}