/**
 * One-off cleanup: split combined collaboration artist rows into the real
 * individual artists they name, and dedupe the result.
 *
 * Background: tags routinely store several artists in one string
 * ("Booker & ЗАМАЙ", "STED.D; Alphavite", "21 Savage & Metro Boomin"). An
 * earlier normalize_artist only handled commas + feat suffixes, so these were
 * persisted as a single bogus artist row — duplicating the real artists (which
 * also exist on their own) and producing two-artists-in-one entries.
 *
 * What this does, per combined artist row C (name splits into >1 artist):
 *   1. Resolve/create a canonical artist row for each component, keyed by
 *      normalize_artist() so "Booker & X" folds into the existing "Booker".
 *   2. For every track linked to C (via track_artists), link it to all the real
 *      components instead: first = 'main', the rest = 'featured'.
 *   3. Reassign C's albums to its primary component.
 *   4. Delete C (cascades its track_artists / artist_similar / aliases).
 * Then a final pass merges any artists that share a normalized_name (preferring
 * the row with an MBID, then a canonical_name).
 *
 * Idempotent: re-running after a clean run is a no-op (no row will split into
 * more than one component once the data is fixed). Wrapped in a transaction —
 * either the whole cleanup commits or nothing does.
 *
 * Run:  DATABASE_URL=... npx tsx src/scripts/split-collab-artists.ts
 *   (or, built)  node dist/scripts/split-collab-artists.js
 * Add --dry-run to print what would change without writing.
 */
import { Client as PgClient } from 'pg';
import { splitArtistNames } from '../utils/artist-names.js';

interface ArtistRow {
  id: string;
  name: string;
  mbid: string | null;
  canonical_name: string | null;
}

const DRY_RUN = process.argv.includes('--dry-run');

async function main() {
  const pg = new PgClient({ connectionString: process.env.DATABASE_URL });
  await pg.connect();
  console.log(`[SplitCollab] Connected${DRY_RUN ? ' (DRY RUN — no writes)' : ''}`);

  const { rows: artists } = await pg.query<ArtistRow>(
    'SELECT id, name, mbid, canonical_name FROM artists'
  );

  // Identify the combined rows: name names more than one artist.
  const combined = artists.filter((a) => splitArtistNames(a.name).length > 1);
  console.log(`[SplitCollab] ${artists.length} artists total, ${combined.length} combined rows to split`);

  if (!DRY_RUN) await pg.query('BEGIN');

  let tracksRelinked = 0;
  let createdArtists = 0;

  /**
   * Resolve a single artist name to a canonical artist id, by normalized_name.
   * Creates the row if no match exists. Returns the id.
   */
  async function resolveArtistId(name: string): Promise<string> {
    const existing = await pg.query(
      `SELECT id FROM artists
       WHERE normalized_name = normalize_artist($1)
       ORDER BY CASE WHEN mbid IS NOT NULL THEN 0 ELSE 1 END,
                CASE WHEN canonical_name IS NOT NULL THEN 0 ELSE 1 END,
                created_at
       LIMIT 1`,
      [name]
    );
    if (existing.rows.length > 0) return existing.rows[0].id;

    if (DRY_RUN) {
      console.log(`[SplitCollab]   would create artist "${name}"`);
      return `dry-${name}`;
    }
    const inserted = await pg.query(
      `INSERT INTO artists (name, canonical_name, sort_name)
       VALUES ($1, $1, $1) RETURNING id`,
      [name]
    );
    createdArtists++;
    return inserted.rows[0].id;
  }

  for (const c of combined) {
    const components = splitArtistNames(c.name);
    console.log(`[SplitCollab] "${c.name}" -> [${components.join(' | ')}]`);

    const componentIds: string[] = [];
    for (const name of components) componentIds.push(await resolveArtistId(name));
    const primaryId = componentIds[0];

    if (DRY_RUN) continue;

    // Tracks currently attributed to the combined row.
    const { rows: links } = await pg.query<{ track_id: string }>(
      'SELECT DISTINCT track_id FROM track_artists WHERE artist_id = $1',
      [c.id]
    );

    for (const { track_id } of links) {
      // primary = main, the rest = featured. Skip self-conflicts.
      await pg.query(
        `INSERT INTO track_artists (track_id, artist_id, role)
         VALUES ($1, $2, 'main') ON CONFLICT (track_id, artist_id, role) DO NOTHING`,
        [track_id, primaryId]
      );
      for (const featId of componentIds.slice(1)) {
        if (featId === primaryId) continue;
        await pg.query(
          `INSERT INTO track_artists (track_id, artist_id, role)
           VALUES ($1, $2, 'featured') ON CONFLICT (track_id, artist_id, role) DO NOTHING`,
          [track_id, featId]
        );
      }
      tracksRelinked++;
    }

    // Reassign the combined row's albums to the primary artist, but only where
    // that wouldn't collide with an existing (primary, title) album. Colliding
    // albums are dropped (the primary already has that album).
    await pg.query(
      `UPDATE albums a SET artist_id = $1
       WHERE a.artist_id = $2
         AND NOT EXISTS (
           SELECT 1 FROM albums b WHERE b.artist_id = $1 AND b.title = a.title
         )`,
      [primaryId, c.id]
    );

    // Drop the combined artist (cascades remaining track_artists/aliases/similar
    // and any colliding albums via ON DELETE CASCADE).
    await pg.query('DELETE FROM artists WHERE id = $1', [c.id]);
  }

  // Final dedupe: collapse any artists sharing a normalized_name into one,
  // preferring the row with an MBID, then a canonical_name, then oldest.
  let dedupMerged = 0;
  if (!DRY_RUN) {
    const { rows: dups } = await pg.query<{ ids: string[] }>(
      `SELECT array_agg(id ORDER BY
                CASE WHEN mbid IS NOT NULL THEN 0 ELSE 1 END,
                CASE WHEN canonical_name IS NOT NULL THEN 0 ELSE 1 END,
                created_at) AS ids
       FROM artists
       GROUP BY normalized_name
       HAVING COUNT(*) > 1`
    );
    for (const { ids } of dups) {
      const keepId = ids[0];
      for (const mergeId of ids.slice(1)) {
        await pg.query(
          `INSERT INTO track_artists (track_id, artist_id, role)
           SELECT track_id, $1, role FROM track_artists WHERE artist_id = $2
           ON CONFLICT (track_id, artist_id, role) DO NOTHING`,
          [keepId, mergeId]
        );
        await pg.query('DELETE FROM track_artists WHERE artist_id = $1', [mergeId]);
        await pg.query(
          `UPDATE albums a SET artist_id = $1
           WHERE a.artist_id = $2
             AND NOT EXISTS (SELECT 1 FROM albums b WHERE b.artist_id = $1 AND b.title = a.title)`,
          [keepId, mergeId]
        );
        await pg.query('DELETE FROM artists WHERE id = $1', [mergeId]);
        dedupMerged++;
      }
    }
  }

  // Drop redundant 'featured' links where the same artist is already 'main' on
  // that track (can happen when a stale pre-split 'main' link coexists with a
  // 'featured' link added from a collaboration row).
  let redundantRemoved = 0;
  if (!DRY_RUN) {
    const res = await pg.query(
      `DELETE FROM track_artists f
       WHERE f.role = 'featured'
         AND EXISTS (
           SELECT 1 FROM track_artists m
           WHERE m.track_id = f.track_id AND m.artist_id = f.artist_id AND m.role = 'main'
         )`
    );
    redundantRemoved = res.rowCount ?? 0;
  }

  if (!DRY_RUN) await pg.query('COMMIT');

  console.log(
    `[SplitCollab] Done: split=${combined.length} relinkedTracks=${tracksRelinked} ` +
      `createdArtists=${createdArtists} dedupMerged=${dedupMerged} redundantFeaturedRemoved=${redundantRemoved}`
  );
  await pg.end();
}

main()
  .then(() => process.exit(0))
  .catch(async (err) => {
    console.error('[SplitCollab] Failed:', err);
    process.exit(1);
  });
