-- scripts/dedup_media_items.sql
--
-- Detects and removes duplicate media_items, re-parenting all child rows
-- (reading progress, highlights, collections, etc.) onto a single survivor
-- before deleting the losers.
--
-- Two kinds of duplicates are handled:
--   1. PATH duplicates  — same (library_id, file_path), multiple rows.
--                         These block the UNIQUE(library_id, file_path)
--                         constraint added by the schema migration.
--   2. CONTENT duplicates — same file_sha256 within a library, different paths.
--                         Same file imported twice under two names.
--
-- This script is IDEMPOTENT: re-running it is a no-op once the data is clean.
-- It is safe to run against any Bookhoard database, before or after upgrading.
--
-- Usage:
--   psql -h <host> -U postgres -d bookhoard -f scripts/dedup_media_items.sql
--
-- The first section is a DRY-RUN report (SELECTs only, no writes). The cleanup
-- runs inside an explicit transaction. Comment out the cleanup block to inspect
-- first.

-- =====================================================================
-- DRY RUN: report duplicates (no writes)
-- ======================================================================

\echo '=== PATH duplicates (library_id + file_path) ==='
SELECT library_id,
       file_path,
       COUNT(*) AS dupes,
       array_agg(id::text) AS media_item_ids,
       array_agg(COALESCE(file_sha256::text, 'NULL')) AS hashes
FROM media_items
GROUP BY library_id, file_path
HAVING COUNT(*) > 1
ORDER BY COUNT(*) DESC;

\echo '=== CONTENT duplicates (same file_sha256 within a library, different paths) ==='
SELECT library_id,
       file_sha256::text AS hash,
       COUNT(*) AS dupes,
       array_agg(file_path) AS paths,
       array_agg(id::text) AS media_item_ids
FROM media_items
WHERE file_sha256 IS NOT NULL
GROUP BY library_id, file_sha256
HAVING COUNT(*) > 1
ORDER BY COUNT(*) DESC;

\echo '=== Child-row counts per duplicate candidate (helps confirm survivor choice) ==='
SELECT mi.id,
       mi.library_id,
       mi.file_path,
       (SELECT COUNT(*) FROM reading_progress   rp WHERE rp.media_item_id = mi.id) AS progress,
       (SELECT COUNT(*) FROM media_highlights    mh WHERE mh.media_item_id = mi.id) AS highlights,
       (SELECT COUNT(*) FROM media_bookmarks     mb WHERE mb.media_item_id = mi.id) AS bookmarks,
       (SELECT COUNT(*) FROM media_notes         mn WHERE mn.media_item_id = mi.id) AS notes,
       (SELECT COUNT(*) FROM reading_history     rh WHERE rh.media_item_id = mi.id) AS history,
       (SELECT COUNT(*) FROM collection_items    ci WHERE ci.media_item_id = mi.id) AS collections
FROM media_items mi
WHERE (mi.library_id, mi.file_path) IN (
    SELECT library_id, file_path FROM media_items
    GROUP BY library_id, file_path HAVING COUNT(*) > 1
)
ORDER BY mi.library_id, mi.file_path, mi.id;

-- =====================================================================
-- HELPER FUNCTIONS (also defined by schema.sql; CREATE OR REPLACE keeps them in sync)
-- ======================================================================

-- Move every child row that points at p_source so it points at p_target,
-- deleting any source rows that would violate a UNIQUE constraint on the
-- target. Idempotent; no-op when p_target = p_source.
CREATE OR REPLACE FUNCTION reparent_media_item_children(p_target UUID, p_source UUID)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
    IF p_target IS NULL OR p_source IS NULL OR p_target = p_source THEN
        RETURN;
    END IF;

    -- reading_progress (UNIQUE media_item_id, user_id)
    DELETE FROM reading_progress
    WHERE media_item_id = p_source
      AND user_id IN (SELECT user_id FROM reading_progress WHERE media_item_id = p_target);
    UPDATE reading_progress SET media_item_id = p_target WHERE media_item_id = p_source;

    -- reading_speed (UNIQUE user_id, media_item_id)
    DELETE FROM reading_speed
    WHERE media_item_id = p_source
      AND user_id IN (SELECT user_id FROM reading_speed WHERE media_item_id = p_target);
    UPDATE reading_speed SET media_item_id = p_target WHERE media_item_id = p_source;

    -- media_ratings (UNIQUE media_item_id, user_id)
    DELETE FROM media_ratings
    WHERE media_item_id = p_source
      AND user_id IN (SELECT user_id FROM media_ratings WHERE media_item_id = p_target);
    UPDATE media_ratings SET media_item_id = p_target WHERE media_item_id = p_source;

    -- media_bookmarks (UNIQUE media_item_id, user_id, title)
    DELETE FROM media_bookmarks
    WHERE media_item_id = p_source
      AND (user_id, title) IN (SELECT user_id, title FROM media_bookmarks WHERE media_item_id = p_target);
    UPDATE media_bookmarks SET media_item_id = p_target WHERE media_item_id = p_source;

    -- media_item_formats (UNIQUE media_item_id, format_type)
    DELETE FROM media_item_formats
    WHERE media_item_id = p_source
      AND format_type IN (SELECT format_type FROM media_item_formats WHERE media_item_id = p_target);
    UPDATE media_item_formats SET media_item_id = p_target WHERE media_item_id = p_source;

    -- collection_items (UNIQUE collection_id, media_item_id)
    DELETE FROM collection_items
    WHERE media_item_id = p_source
      AND collection_id IN (SELECT collection_id FROM collection_items WHERE media_item_id = p_target);
    UPDATE collection_items SET media_item_id = p_target WHERE media_item_id = p_source;

    -- kobo_shelves (UNIQUE device_id, media_item_id)
    DELETE FROM kobo_shelves
    WHERE media_item_id = p_source
      AND device_id IN (SELECT device_id FROM kobo_shelves WHERE media_item_id = p_target);
    UPDATE kobo_shelves SET media_item_id = p_target WHERE media_item_id = p_source;

    -- panel_data (UNIQUE media_item_id, page_number)
    DELETE FROM panel_data
    WHERE media_item_id = p_source
      AND page_number IN (SELECT page_number FROM panel_data WHERE media_item_id = p_target);
    UPDATE panel_data SET media_item_id = p_target WHERE media_item_id = p_source;

    -- processing_issues (UNIQUE media_item_id, issue_type)
    DELETE FROM processing_issues
    WHERE media_item_id = p_source
      AND issue_type IN (SELECT issue_type FROM processing_issues WHERE media_item_id = p_target);
    UPDATE processing_issues SET media_item_id = p_target WHERE media_item_id = p_source;

    -- device_file_aliases (UNIQUE device_id, file_path) — paths may collide
    DELETE FROM device_file_aliases
    WHERE media_item_id = p_source
      AND (device_id, file_path) IN (SELECT device_id, file_path FROM device_file_aliases WHERE media_item_id = p_target);
    UPDATE device_file_aliases SET media_item_id = p_target WHERE media_item_id = p_source;

    -- Tables whose UNIQUE keys do not include media_item_id: plain re-parent.
    UPDATE device_catalogs   SET media_item_id = p_target WHERE media_item_id = p_source;
    UPDATE kobo_entitlements SET media_item_id = p_target WHERE media_item_id = p_source;
    UPDATE media_highlights  SET media_item_id = p_target WHERE media_item_id = p_source;
    UPDATE media_notes       SET media_item_id = p_target WHERE media_item_id = p_source;
    UPDATE reading_history   SET media_item_id = p_target WHERE media_item_id = p_source;
    UPDATE sync_conflicts    SET media_item_id = p_target WHERE media_item_id = p_source;
    UPDATE sync_queue        SET media_item_id = p_target WHERE media_item_id = p_source;
END;
$$;

-- Collapse every (library_id, file_path) group into a single row.
-- Survivor = the row with the most user data; ties broken by lowest id.
CREATE OR REPLACE FUNCTION dedup_media_items_by_path() RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
    g        RECORD;
    v_surv   UUID;
    v_loser  UUID;
BEGIN
    FOR g IN
        SELECT library_id, file_path
        FROM media_items
        GROUP BY library_id, file_path
        HAVING COUNT(*) > 1
    LOOP
        SELECT mi.id INTO v_surv
        FROM media_items mi
        WHERE mi.library_id = g.library_id AND mi.file_path = g.file_path
        ORDER BY
            ((SELECT COUNT(*) FROM reading_progress  rp WHERE rp.media_item_id = mi.id)
           + (SELECT COUNT(*) FROM media_highlights   mh WHERE mh.media_item_id = mi.id)
           + (SELECT COUNT(*) FROM media_bookmarks    mb WHERE mb.media_item_id = mi.id)
           + (SELECT COUNT(*) FROM media_notes        mn WHERE mn.media_item_id = mi.id)
           + (SELECT COUNT(*) FROM reading_history    rh WHERE rh.media_item_id = mi.id)
           + (SELECT COUNT(*) FROM collection_items   ci WHERE ci.media_item_id = mi.id)) DESC,
            mi.id ASC
        LIMIT 1;

        FOR v_loser IN
            SELECT id FROM media_items
            WHERE library_id = g.library_id AND file_path = g.file_path AND id <> v_surv
            ORDER BY id
        LOOP
            PERFORM reparent_media_item_children(v_surv, v_loser);
            DELETE FROM media_items WHERE id = v_loser;
        END LOOP;
    END LOOP;
END;
$$;

-- ======================================================================
-- CLEANUP: collapse path duplicates (required before the UNIQUE constraint)
-- ======================================================================

\echo '=== Collapsing path duplicates ===';
BEGIN;
    SELECT dedup_media_items_by_path();
COMMIT;

\echo '=== Done. Remaining PATH duplicates (should be empty): ===';
SELECT library_id, file_path, COUNT(*) AS dupes
FROM media_items
GROUP BY library_id, file_path
HAVING COUNT(*) > 1;

\echo 'NOTE: CONTENT duplicates (same hash, different paths) are NOT auto-deleted.'
\echo '      They do not violate the UNIQUE constraint. Review the dry-run output'
\echo '      above and merge them manually if desired.'
