using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
    /// <inheritdoc />
    public partial class Add_LibraryFolder_PathHash_UniqueIndex : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            // ersatztv#491 — audit/clean pre-existing duplicate LibraryFolder rows before the unique index.
            // Duplicates were reachable before #488 (the folder lookup read a scan-start in-memory snapshot,
            // so a folder created earlier in the SAME scan was invisible and inserted again) and via the
            // check-then-insert race the index now closes. Keep the lowest Id per (LibraryPathId, Path) and
            // repoint every dependent row at it before deleting the losers. The helper tables keep the
            // statements readable; DROP TABLE IF EXISTS makes a retry after a partial failure safe.
            migrationBuilder.Sql("DROP TABLE IF EXISTS __LibraryFolderDedupe");
            migrationBuilder.Sql(
                """
                CREATE TABLE __LibraryFolderDedupe (
                    LoserId INTEGER NOT NULL PRIMARY KEY,
                    KeeperId INTEGER NOT NULL
                )
                """);

            migrationBuilder.Sql(
                """
                INSERT INTO __LibraryFolderDedupe (LoserId, KeeperId)
                SELECT l.Id, k.KeeperId
                FROM LibraryFolder l
                INNER JOIN (
                    SELECT LibraryPathId, Path, MIN(Id) AS KeeperId
                    FROM LibraryFolder
                    GROUP BY LibraryPathId, Path
                ) k ON k.LibraryPathId = l.LibraryPathId AND k.Path = l.Path
                WHERE l.Id <> k.KeeperId
                """);

            // media files recorded against a duplicate folder follow the keeper (MediaFile.LibraryFolderId
            // is Restrict, so the delete below would fail otherwise)
            migrationBuilder.Sql(
                """
                UPDATE MediaFile
                SET LibraryFolderId = (
                    SELECT KeeperId FROM __LibraryFolderDedupe WHERE LoserId = MediaFile.LibraryFolderId)
                WHERE LibraryFolderId IN (SELECT LoserId FROM __LibraryFolderDedupe)
                """);

            // child folders parented on a duplicate follow the keeper (ParentId is Restrict as well)
            migrationBuilder.Sql(
                """
                UPDATE LibraryFolder
                SET ParentId = (
                    SELECT KeeperId FROM __LibraryFolderDedupe WHERE LoserId = LibraryFolder.ParentId)
                WHERE ParentId IN (SELECT LoserId FROM __LibraryFolderDedupe)
                """);

            // A folder parented on its OWN duplicate would become its own parent above. Unreachable from
            // any code path today, but this is a tree the scanner walks, so remove the cycle class rather
            // than reason about it.
            migrationBuilder.Sql("UPDATE LibraryFolder SET ParentId = NULL WHERE ParentId = Id");

            // Clear the survivor's etag. Which duplicate the scanner was actually writing to was
            // arbitrary, so MIN(Id)'s etag may describe a stale view of the folder and would suppress the
            // next rescan. A null etag costs exactly one rescan and cannot be wrong.
            migrationBuilder.Sql(
                "UPDATE LibraryFolder SET Etag = NULL WHERE Id IN (SELECT KeeperId FROM __LibraryFolderDedupe)");

            // ImageFolderDuration is 1:1 with LibraryFolder (unique index on LibraryFolderId), so the
            // duplicates' rows cannot all be repointed. Keep the keeper's own setting when it has one;
            // otherwise promote exactly one loser's (lowest Id) and drop the rest.
            migrationBuilder.Sql("DROP TABLE IF EXISTS __LibraryFolderDedupeIfd");
            migrationBuilder.Sql(
                """
                CREATE TABLE __LibraryFolderDedupeIfd (
                    KeeperId INTEGER NOT NULL PRIMARY KEY,
                    IfdId INTEGER NOT NULL
                )
                """);

            migrationBuilder.Sql(
                """
                INSERT INTO __LibraryFolderDedupeIfd (KeeperId, IfdId)
                SELECT d.KeeperId, MIN(i.Id)
                FROM __LibraryFolderDedupe d
                INNER JOIN ImageFolderDuration i ON i.LibraryFolderId = d.LoserId
                WHERE NOT EXISTS (
                    SELECT 1 FROM ImageFolderDuration ki WHERE ki.LibraryFolderId = d.KeeperId)
                GROUP BY d.KeeperId
                """);

            migrationBuilder.Sql(
                """
                DELETE FROM ImageFolderDuration
                WHERE LibraryFolderId IN (SELECT LoserId FROM __LibraryFolderDedupe)
                  AND Id NOT IN (SELECT IfdId FROM __LibraryFolderDedupeIfd)
                """);

            migrationBuilder.Sql(
                """
                UPDATE ImageFolderDuration
                SET LibraryFolderId = (
                    SELECT KeeperId FROM __LibraryFolderDedupeIfd WHERE IfdId = ImageFolderDuration.Id)
                WHERE Id IN (SELECT IfdId FROM __LibraryFolderDedupeIfd)
                """);

            migrationBuilder.Sql(
                "DELETE FROM LibraryFolder WHERE Id IN (SELECT LoserId FROM __LibraryFolderDedupe)");

            migrationBuilder.Sql("DROP TABLE __LibraryFolderDedupeIfd");
            migrationBuilder.Sql("DROP TABLE __LibraryFolderDedupe");

            migrationBuilder.AddColumn<string>(
                name: "PathHash",
                table: "LibraryFolder",
                type: "TEXT",
                maxLength: 64,
                nullable: true);

            // Existing rows keep a null hash on purpose: a unique index treats nulls as distinct, so the
            // index applies cleanly to any database, and LibraryRepository.GetOrAddFolder heals each row
            // (SHA-256 of Path) the first time a scan touches it. Those rows are deduplicated above and are
            // still found by the Path lookup, so no insert can race them in the meantime.
            //
            // Create-then-drop rather than EF's scaffolded drop-then-create, matching the MySql copy, where
            // InnoDB refuses to drop the foreign key's only backing index.
            migrationBuilder.CreateIndex(
                name: "IX_LibraryFolder_LibraryPathId_PathHash",
                table: "LibraryFolder",
                columns: new[] { "LibraryPathId", "PathHash" },
                unique: true);

            migrationBuilder.DropIndex(
                name: "IX_LibraryFolder_LibraryPathId",
                table: "LibraryFolder");
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateIndex(
                name: "IX_LibraryFolder_LibraryPathId",
                table: "LibraryFolder",
                column: "LibraryPathId");

            migrationBuilder.DropIndex(
                name: "IX_LibraryFolder_LibraryPathId_PathHash",
                table: "LibraryFolder");

            migrationBuilder.DropColumn(
                name: "PathHash",
                table: "LibraryFolder");
        }
    }
}
