import { MigrationInterface, QueryRunner } from 'typeorm';

type LegacyServerRow = {
  id: string;
  tags: string | null;
  channels: string | null;
};

type LegacyServerChannel = {
  id?: unknown;
  name?: unknown;
  type?: unknown;
  position?: unknown;
};

function parseArray<T>(raw: string | null): T[] {
  try {
    const parsed = JSON.parse(raw || '[]');

    return Array.isArray(parsed) ? parsed as T[] : [];
  } catch {
    return [];
  }
}

function normalizeChannelName(name: string): string {
  return name.trim().replace(/\s+/g, ' ');
}

function channelNameKey(type: 'text' | 'voice', name: string): string {
  return `${type}:${normalizeChannelName(name).toLocaleLowerCase()}`;
}

function isFiniteNumber(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value);
}

function normalizeServerTags(raw: string | null): string[] {
  return parseArray<unknown>(raw).filter((tag): tag is string => typeof tag === 'string');
}

function normalizeServerChannels(raw: string | null) {
  const channels = parseArray<LegacyServerChannel>(raw);
  const seenIds = new Set<string>();
  const seenNames = new Set<string>();

  return channels.flatMap((channel, index) => {
    const id = typeof channel.id === 'string' ? channel.id.trim() : '';
    const name = typeof channel.name === 'string' ? normalizeChannelName(channel.name) : '';
    const type = channel.type === 'text' || channel.type === 'voice' ? channel.type : null;
    const position = isFiniteNumber(channel.position) ? channel.position : index;
    const nameKey = type ? channelNameKey(type, name) : '';

    if (!id || !name || !type || seenIds.has(id) || seenNames.has(nameKey)) {
      return [];
    }

    seenIds.add(id);
    seenNames.add(nameKey);

    return [{
      channelId: id,
      name,
      type,
      position
    }];
  });
}

export class NormalizeServerArrays1000000000004 implements MigrationInterface {
  name = 'NormalizeServerArrays1000000000004';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      CREATE TABLE IF NOT EXISTS "server_tags" (
        "serverId"  TEXT NOT NULL,
        "position"  INTEGER NOT NULL,
        "value"     TEXT NOT NULL,
        PRIMARY KEY ("serverId", "position")
      )
    `);
    await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_server_tags_serverId" ON "server_tags" ("serverId")`);

    await queryRunner.query(`
      CREATE TABLE IF NOT EXISTS "server_channels" (
        "serverId"  TEXT NOT NULL,
        "channelId" TEXT NOT NULL,
        "name"      TEXT NOT NULL,
        "type"      TEXT NOT NULL,
        "position"  INTEGER NOT NULL,
        PRIMARY KEY ("serverId", "channelId")
      )
    `);
    await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_server_channels_serverId" ON "server_channels" ("serverId")`);

    const rows = await queryRunner.query(`SELECT "id", "tags", "channels" FROM "servers"`) as LegacyServerRow[];

    for (const row of rows) {
      for (const [position, tag] of normalizeServerTags(row.tags).entries()) {
        await queryRunner.query(
          `INSERT OR REPLACE INTO "server_tags" ("serverId", "position", "value") VALUES (?, ?, ?)`,
          [row.id, position, tag]
        );
      }

      for (const channel of normalizeServerChannels(row.channels)) {
        await queryRunner.query(
          `INSERT OR REPLACE INTO "server_channels" ("serverId", "channelId", "name", "type", "position") VALUES (?, ?, ?, ?, ?)`,
          [row.id, channel.channelId, channel.name, channel.type, channel.position]
        );
      }
    }

    await queryRunner.query(`
      CREATE TABLE "servers_next" (
        "id"             TEXT PRIMARY KEY NOT NULL,
        "name"           TEXT NOT NULL,
        "description"    TEXT,
        "ownerId"        TEXT NOT NULL,
        "ownerPublicKey" TEXT NOT NULL,
        "isPrivate"      INTEGER NOT NULL DEFAULT 0,
        "maxUsers"       INTEGER NOT NULL DEFAULT 0,
        "currentUsers"   INTEGER NOT NULL DEFAULT 0,
        "createdAt"      INTEGER NOT NULL,
        "lastSeen"       INTEGER NOT NULL,
        "passwordHash"   TEXT
      )
    `);
    await queryRunner.query(`
      INSERT INTO "servers_next" ("id", "name", "description", "ownerId", "ownerPublicKey", "isPrivate", "maxUsers", "currentUsers", "createdAt", "lastSeen", "passwordHash")
      SELECT "id", "name", "description", "ownerId", "ownerPublicKey", "isPrivate", "maxUsers", "currentUsers", "createdAt", "lastSeen", "passwordHash"
      FROM "servers"
    `);
    await queryRunner.query(`DROP TABLE "servers"`);
    await queryRunner.query(`ALTER TABLE "servers_next" RENAME TO "servers"`);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE IF EXISTS "server_channels"`);
    await queryRunner.query(`DROP TABLE IF EXISTS "server_tags"`);
  }
}
