<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Domain\Blog\ContentFormat;
use App\Domain\Blog\ContentRenderer;
use App\Models\Article;
use App\Models\Attachment;
use App\Models\Category;
use App\Models\Comment;
use App\Models\Link;
use App\Models\Stylevar;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Throwable;

class SablogImportCommand extends Command
{
    protected $signature = 'sablog:import
        {--connection=sablog : Laravel DB connection name for the source sablog database}
        {--prefix=sablog_ : Source table prefix}
        {--attachments= : Absolute path to sablog attachments directory}
        {--mode=raw : Import mode: raw (keep HTML) or markdown (convert + rewrite attach embeds)}
        {--encoding=auto : utf8|gbk|auto}
        {--disk=attachments : Target attachments disk}
        {--attachments-url-prefix=attachments : Legacy URL prefix}
        {--retry-failed : Retry attachment rows that failed previously}
        {--dry-run : Do not write to the target database or object storage}';

    protected $description = 'Import sablog content. Modes: raw (HTML) or markdown (converted).';

    /** @var array<string, int> */
    protected array $report = [
        'users' => 0,
        'categories' => 0,
        'articles' => 0,
        'comments' => 0,
        'tags' => 0,
        'links' => 0,
        'stylevars' => 0,
        'attachments_ok' => 0,
        'attachments_missing' => 0,
        'attachments_failed' => 0,
        'skipped_trackbacks' => 0,
        'skipped_searchindex' => 0,
        'skipped_sessions' => 0,
    ];

    public function handle(ContentRenderer $renderer): int
    {
        $mode = strtolower((string) $this->option('mode'));
        if (! in_array($mode, ['raw', 'markdown'], true)) {
            $this->error('--mode must be raw or markdown');

            return self::FAILURE;
        }

        $connection = (string) $this->option('connection');
        $prefix = (string) $this->option('prefix');
        $attachmentsRoot = $this->option('attachments');
        $disk = (string) $this->option('disk');
        $dryRun = (bool) $this->option('dry-run');
        $encoding = (string) $this->option('encoding');

        if (! config("database.connections.{$connection}")) {
            $this->error("Database connection [{$connection}] is not configured. Add it to config/database.php / .env (SABLOG_DB_*).");

            return self::FAILURE;
        }

        $this->info("Import mode: {$mode}".($dryRun ? ' (dry-run)' : ''));

        try {
            $source = DB::connection($connection);
            $source->select('select 1');
        } catch (Throwable $e) {
            $this->error('Cannot connect to sablog database: '.$e->getMessage());

            return self::FAILURE;
        }

        $this->reportSkipped($source, $prefix);

        if (! $dryRun) {
            DB::transaction(function () use ($source, $prefix, $encoding, $mode, $renderer, $attachmentsRoot, $disk) {
                $this->importUsers($source, $prefix, $encoding);
                $this->importCategories($source, $prefix, $encoding);
                $this->importArticles($source, $prefix, $encoding, $mode, $renderer);
                $this->importComments($source, $prefix, $encoding);
                $this->importTags($source, $prefix, $encoding);
                $this->importLinks($source, $prefix, $encoding);
                $this->importStylevars($source, $prefix, $encoding);
                $this->importAttachments($source, $prefix, $attachmentsRoot, $disk);
            });
        } else {
            $this->importUsers($source, $prefix, $encoding, true);
            $this->importCategories($source, $prefix, $encoding, true);
            $this->importArticles($source, $prefix, $encoding, $mode, $renderer, true);
            $this->countTable($source, $prefix.'comments', 'comments');
            $this->countTable($source, $prefix.'tags', 'tags');
            $this->countTable($source, $prefix.'links', 'links');
            $this->countTable($source, $prefix.'stylevars', 'stylevars');
            $this->countTable($source, $prefix.'attachments', 'attachments_ok');
        }

        $this->table(array_keys($this->report), [array_values($this->report)]);
        $this->info('Done. Source database/files were not modified.');

        return self::SUCCESS;
    }

    protected function reportSkipped($source, string $prefix): void
    {
        foreach ([
            'trackbacks' => 'skipped_trackbacks',
            'trackbacklog' => 'skipped_trackbacks',
            'searchindex' => 'skipped_searchindex',
            'sessions' => 'skipped_sessions',
        ] as $table => $key) {
            $name = $prefix.$table;
            if ($this->sourceTableExists($source, $name)) {
                $this->report[$key] += (int) $source->table($name)->count();
            }
        }
    }

    protected function importUsers($source, string $prefix, string $encoding, bool $countOnly = false): void
    {
        $table = $prefix.'users';
        if (! $this->sourceTableExists($source, $table)) {
            return;
        }

        foreach ($source->table($table)->orderBy('userid')->cursor() as $row) {
            $this->report['users']++;
            if ($countOnly) {
                continue;
            }

            $this->upsertWithId(User::class, (int) $row->userid, [
                'name' => $this->decode((string) $row->username, $encoding),
                'username' => $this->decode((string) $row->username, $encoding),
                'email' => null,
                'password' => Hash::make(Str::password(32)),
                'password_legacy' => (string) $row->password,
                'url' => $this->decode((string) ($row->url ?? ''), $encoding) ?: null,
                'login_count' => (int) ($row->logincount ?? 0),
                'login_ip' => $row->loginip ?? null,
                'login_at' => $this->fromUnix($row->logintime ?? null),
                'reg_ip' => $row->regip ?? null,
                'created_at' => $this->fromUnix($row->regdateline ?? null) ?? now(),
                'updated_at' => now(),
            ]);
        }
    }

    protected function importCategories($source, string $prefix, string $encoding, bool $countOnly = false): void
    {
        $table = $prefix.'categories';
        if (! $this->sourceTableExists($source, $table)) {
            return;
        }

        foreach ($source->table($table)->orderBy('cid')->cursor() as $row) {
            $this->report['categories']++;
            if ($countOnly) {
                continue;
            }

            $this->upsertWithId(Category::class, (int) $row->cid, [
                'name' => $this->decode((string) $row->name, $encoding),
                'display_order' => (int) ($row->displayorder ?? 0),
                'articles_count' => (int) ($row->articles ?? 0),
            ]);
        }
    }

    protected function importArticles($source, string $prefix, string $encoding, string $mode, ContentRenderer $renderer, bool $countOnly = false): void
    {
        $table = $prefix.'articles';
        if (! $this->sourceTableExists($source, $table)) {
            return;
        }

        foreach ($source->table($table)->orderBy('articleid')->cursor() as $row) {
            $this->report['articles']++;
            if ($countOnly) {
                continue;
            }

            $content = $this->decode((string) $row->content, $encoding);
            $format = ContentFormat::HTML;

            if ($mode === 'markdown') {
                $content = $renderer->convertImportedHtmlToMarkdown($content, (int) $row->articleid);
                $format = ContentFormat::MARKDOWN;
            }

            $legacyAttachments = null;
            if (! empty($row->attachments)) {
                $legacyAttachments = @unserialize($row->attachments);
                if ($legacyAttachments === false) {
                    $legacyAttachments = ['raw' => $row->attachments];
                }
            }

            $this->upsertWithId(Article::class, (int) $row->articleid, [
                'category_id' => (int) $row->cid,
                'user_id' => (int) $row->uid,
                'title' => $this->decode((string) $row->title, $encoding),
                'content' => $content,
                'content_format' => $format,
                'description' => $this->decode((string) ($row->description ?? ''), $encoding) ?: null,
                'keywords' => $this->decode((string) ($row->keywords ?? ''), $encoding) ?: null,
                'published_at' => $this->fromUnix($row->dateline ?? null),
                'views' => (int) ($row->views ?? 0),
                'comments_count' => (int) ($row->comments ?? 0),
                'stick' => (bool) ($row->stick ?? false),
                'visible' => (bool) ($row->visible ?? true),
                'close_comment' => (bool) ($row->closecomment ?? false),
                'read_password' => ($row->readpassword ?? '') !== '' ? (string) $row->readpassword : null,
                'legacy_attachments' => $legacyAttachments,
            ]);
        }
    }

    protected function importComments($source, string $prefix, string $encoding): void
    {
        $table = $prefix.'comments';
        if (! $this->sourceTableExists($source, $table)) {
            return;
        }

        foreach ($source->table($table)->orderBy('commentid')->cursor() as $row) {
            $this->report['comments']++;

            // Avoid plugin hooks (e.g. AI moderation) rewriting imported statuses / flooding queues.
            Comment::withoutEvents(function () use ($row, $encoding): void {
                $this->upsertWithId(Comment::class, (int) $row->commentid, [
                    'article_id' => (int) $row->articleid,
                    'author' => $this->decode((string) $row->author, $encoding),
                    'url' => $this->decode((string) ($row->url ?? ''), $encoding) ?: null,
                    'content' => $this->decode((string) $row->content, $encoding),
                    'ip' => $row->ipaddress ?? null,
                    'moderation_status' => ((int) ($row->visible ?? 0)) === 1
                        ? Comment::STATUS_APPROVED
                        : Comment::STATUS_PENDING,
                    'published_at' => $this->fromUnix($row->dateline ?? null),
                ]);
            });
        }
    }

    protected function importTags($source, string $prefix, string $encoding): void
    {
        $table = $prefix.'tags';
        if (! $this->sourceTableExists($source, $table)) {
            return;
        }

        foreach ($source->table($table)->orderBy('tagid')->cursor() as $row) {
            $this->report['tags']++;

            $tag = $this->upsertWithId(Tag::class, (int) $row->tagid, [
                'name' => $this->decode((string) $row->tag, $encoding),
                'use_count' => (int) ($row->usenum ?? 0),
            ]);

            $ids = preg_split('/\s*,\s*/', (string) ($row->aids ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [];
            $articleIds = collect($ids)->map(fn ($id) => (int) $id)->filter()->unique()->values()->all();
            $tag->articles()->sync($articleIds);
        }
    }

    protected function importLinks($source, string $prefix, string $encoding): void
    {
        $table = $prefix.'links';
        if (! $this->sourceTableExists($source, $table)) {
            return;
        }

        foreach ($source->table($table)->orderBy('linkid')->cursor() as $row) {
            $this->report['links']++;

            $this->upsertWithId(Link::class, (int) $row->linkid, [
                'name' => $this->decode((string) $row->name, $encoding),
                'url' => (string) $row->url,
                'note' => $this->decode((string) ($row->note ?? ''), $encoding) ?: null,
                'display_order' => (int) ($row->displayorder ?? 0),
                'visible' => (bool) ($row->visible ?? true),
            ]);
        }
    }

    protected function importStylevars($source, string $prefix, string $encoding): void
    {
        $table = $prefix.'stylevars';
        if (! $this->sourceTableExists($source, $table)) {
            return;
        }

        foreach ($source->table($table)->orderBy('stylevarid')->cursor() as $row) {
            $this->report['stylevars']++;

            $this->upsertWithId(Stylevar::class, (int) $row->stylevarid, [
                'title' => $this->decode((string) $row->title, $encoding),
                'value' => $this->decode((string) ($row->value ?? ''), $encoding),
                'visible' => (bool) ($row->visible ?? true),
            ]);
        }
    }

    protected function importAttachments($source, string $prefix, ?string $attachmentsRoot, string $disk): void
    {
        $table = $prefix.'attachments';
        if (! $this->sourceTableExists($source, $table)) {
            return;
        }

        $retryFailed = (bool) $this->option('retry-failed');

        foreach ($source->table($table)->orderBy('attachmentid')->cursor() as $row) {
            $id = (int) $row->attachmentid;
            $legacy = ltrim(str_replace('\\', '/', (string) $row->filepath), '/');
            $existing = Attachment::query()->find($id);

            if ($existing && $existing->synced_at && ! $retryFailed) {
                $this->report['attachments_ok']++;

                continue;
            }

            $local = $attachmentsRoot
                ? rtrim($attachmentsRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $legacy)
                : null;

            $articleId = (int) ($row->articleid ?? 0) ?: null;
            $filename = (string) ($row->filename ?: basename($legacy));
            $key = sprintf(
                'attachments/%s/%d/%s.%s',
                $articleId ?: 'orphan',
                $id,
                substr(hash('sha256', $legacy.$id), 0, 16),
                pathinfo($filename, PATHINFO_EXTENSION) ?: 'bin'
            );

            $payload = [
                'article_id' => $articleId,
                'disk' => $disk,
                'path' => $key,
                'thumb_path' => null,
                'filename' => $filename,
                'mime' => $row->filetype ?: null,
                'size' => (int) ($row->filesize ?? 0),
                'checksum' => null,
                'visibility' => Attachment::VISIBILITY_PUBLIC,
                'legacy_filepath' => $legacy,
                'downloads' => (int) ($row->downloads ?? 0),
                'synced_at' => null,
            ];

            if (! $local || ! is_file($local)) {
                $this->report['attachments_missing']++;
                $this->upsertWithId(Attachment::class, $id, $payload);

                continue;
            }

            try {
                $payload['checksum'] = hash_file('sha256', $local) ?: null;
                $payload['size'] = filesize($local) ?: $payload['size'];
                $payload['mime'] = mime_content_type($local) ?: $payload['mime'];

                $stream = fopen($local, 'r');
                Storage::disk($disk)->put($key, $stream, ['visibility' => 'public']);
                if (is_resource($stream)) {
                    fclose($stream);
                }

                $thumbLegacy = ltrim(str_replace('\\', '/', (string) ($row->thumb_filepath ?? '')), '/');
                if ($attachmentsRoot && $thumbLegacy !== '') {
                    $thumbLocal = rtrim($attachmentsRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $thumbLegacy);
                    if (is_file($thumbLocal)) {
                        $thumbKey = preg_replace('/(\.[^.]+)?$/', '_thumb$1', $key) ?: $key.'_thumb';
                        $thumbStream = fopen($thumbLocal, 'r');
                        Storage::disk($disk)->put($thumbKey, $thumbStream, ['visibility' => 'public']);
                        if (is_resource($thumbStream)) {
                            fclose($thumbStream);
                        }
                        $payload['thumb_path'] = $thumbKey;
                    }
                }

                $payload['synced_at'] = now();
                $this->upsertWithId(Attachment::class, $id, $payload);
                $this->report['attachments_ok']++;
            } catch (Throwable $e) {
                $this->report['attachments_failed']++;
                $this->warn("Attachment #{$id} failed: ".$e->getMessage());
                $this->upsertWithId(Attachment::class, $id, $payload);
            }
        }
    }

    /**
     * @param  class-string<Model>  $modelClass
     * @param  array<string, mixed>  $values
     */
    protected function upsertWithId(string $modelClass, int $id, array $values): Model
    {
        /** @var Model $model */
        $model = $modelClass::query()->find($id) ?? new $modelClass;
        $model->forceFill(['id' => $id] + $values)->save();

        return $model;
    }

    protected function countTable($source, string $table, string $key): void
    {
        if ($this->sourceTableExists($source, $table)) {
            $this->report[$key] = (int) $source->table($table)->count();
        }
    }

    protected function sourceTableExists($source, string $table): bool
    {
        try {
            return Schema::connection($source->getName())->hasTable($table);
        } catch (Throwable) {
            return false;
        }
    }

    protected function decode(string $value, string $encoding): string
    {
        if ($value === '') {
            return $value;
        }

        $encoding = strtolower($encoding);
        if ($encoding === 'utf8' || $encoding === 'utf-8') {
            return $value;
        }

        if ($encoding === 'gbk' || $encoding === 'gb2312') {
            return mb_convert_encoding($value, 'UTF-8', 'GBK');
        }

        if (! mb_check_encoding($value, 'UTF-8')) {
            $converted = @mb_convert_encoding($value, 'UTF-8', 'GBK');

            return $converted !== false ? $converted : $value;
        }

        return $value;
    }

    protected function fromUnix(mixed $timestamp): ?\Illuminate\Support\Carbon
    {
        $ts = (int) $timestamp;

        return $ts > 0 ? \Illuminate\Support\Carbon::createFromTimestamp($ts) : null;
    }
}
