-- name: CreateRepository :one
INSERT INTO repositories (name, public) VALUES ($1, $2) RETURNING id;

-- name: GrantRepositoryAccess :exec
INSERT INTO repository_access (repository_id, user_id) VALUES ($1, $2)
ON CONFLICT (repository_id, user_id) DO NOTHING;

-- name: RevokeRepositoryAccess :exec
DELETE FROM repository_access WHERE repository_id = $1 AND user_id = $2;

-- name: CheckUserRepositoryAccess :one
SELECT EXISTS (
    SELECT 1 FROM repository_access
    WHERE repository_id = $1 AND user_id = $2
) AS has_access;

-- name: GetUserAccessibleRepositories :many
SELECT r.* FROM repositories r
LEFT JOIN repository_access ra ON r.id = ra.repository_id
WHERE r.public = TRUE OR ra.user_id = $1
GROUP BY r.id
ORDER BY r.name;

-- name: GetRepositoryUsers :many
SELECT u.* FROM users u
JOIN repository_access ra ON u.id = ra.user_id
WHERE ra.repository_id = $1
ORDER BY u.username;

-- name: GetPublicRepositories :many
SELECT * FROM repositories
WHERE public = TRUE
ORDER BY name;

-- name: GetRepository :one
SELECT * FROM repositories WHERE id = $1;

-- name: GetRepositoryByName :one
SELECT * FROM repositories WHERE name = $1;

-- name: GetRepositoryFiles :many
SELECT DISTINCT f.name, f.executable, f.content_hash, f.conflict
FROM files f
JOIN change_files cf ON f.id = cf.file_id
JOIN changes c ON cf.change_id = c.id
JOIN bookmarks b ON c.id = b.change_id
WHERE c.repository_id = $1 AND b.name = @bookmark
ORDER BY f.name;

-- name: GetRepositoryFilesForRevisionFuzzy :many
WITH target_change AS (
    -- First priority: exact bookmark match
    SELECT b.change_id AS id, 1 AS priority
    FROM bookmarks b
    WHERE b.repository_id = $1
      AND b.name = @revision::text

    UNION ALL

    -- Second priority: exact change name match
    SELECT c.id, 2 AS priority
    FROM changes c
    WHERE c.repository_id = $1
      AND c.name = @revision::text

    UNION ALL

    -- Third priority: change name prefix match
    SELECT c.id, 3 AS priority
    FROM changes c
    WHERE c.repository_id = $1
      AND c.name LIKE @revision::text || '%'
      AND c.name != @revision::text  -- Exclude exact matches (already covered above)
)
SELECT DISTINCT f.*
FROM files f
JOIN change_files cf ON f.id = cf.file_id
JOIN target_change tc ON cf.change_id = tc.id
WHERE tc.id = (SELECT id FROM target_change ORDER BY priority ASC LIMIT 1)
ORDER BY f.name;

-- name: GetRepositoryIgnoreFilesForRevisionFuzzy :many
WITH target_change AS (
    -- First priority: exact bookmark match
    SELECT b.change_id AS id, 1 AS priority
    FROM bookmarks b
    WHERE b.repository_id = $1
      AND b.name = @revision::text

    UNION ALL

    -- Second priority: exact change name match
    SELECT c.id, 2 AS priority
    FROM changes c
    WHERE c.repository_id = $1
      AND c.name = @revision::text

    UNION ALL

    -- Third priority: change name prefix match
    SELECT c.id, 3 AS priority
    FROM changes c
    WHERE c.repository_id = $1
      AND c.name LIKE @revision::text || '%'
      AND c.name != @revision::text  -- Exclude exact matches (already covered above)
)
SELECT DISTINCT f.*
FROM files f
JOIN change_files cf ON f.id = cf.file_id
JOIN target_change tc ON cf.change_id = tc.id
WHERE tc.id = (SELECT id FROM target_change ORDER BY priority ASC LIMIT 1)
  AND (f.name LIKE '%.gitignore' OR f.name LIKE '%.pogoignore')
ORDER BY f.name;

-- name: GetRepositoryIgnoreFilesForChangeId :many
SELECT DISTINCT f.*
FROM files f
JOIN change_files cf ON f.id = cf.file_id
WHERE cf.change_id = $1
  AND (f.name LIKE '%.gitignore' OR f.name LIKE '%.pogoignore')
ORDER BY f.name;

-- name: GetRepositoryBookmarkFileByName :one
SELECT DISTINCT f.name, f.executable, f.content_hash, f.conflict
FROM files f
JOIN change_files cf ON f.id = cf.file_id
JOIN changes c ON cf.change_id = c.id
JOIN bookmarks b ON c.id = b.change_id
WHERE c.repository_id = $1 AND b.name = @bookmark::text AND f.name ILIKE @filename::text || '%'
LIMIT 1;

-- name: CreateUser :one
INSERT INTO users (username) VALUES ($1) RETURNING id;

-- name: GetUserByUsername :one
SELECT * FROM users WHERE username = $1;

-- name: GetUser :one
SELECT * FROM users WHERE id = $1;

-- name: CreatePersonalAccessToken :one
INSERT INTO personal_access_tokens (token, user_id) 
VALUES ($1, $2) RETURNING id;

-- name: GetUserByToken :one
SELECT u.* FROM users u
JOIN personal_access_tokens pat ON u.id = pat.user_id
WHERE pat.token = $1;

-- name: CountUsers :one
SELECT COUNT(*) FROM users;

-- name: CreateUserWithToken :exec
WITH new_user AS (
    INSERT INTO users (username) VALUES ($1) RETURNING id
)
INSERT INTO personal_access_tokens (token, user_id)
SELECT $2, id FROM new_user;

-- name: CreateInitChange :one
INSERT INTO changes (repository_id, name, description, author_id, depth)
VALUES ($1, $2, 'init', $3, 0)
RETURNING id;

-- name: CreateChange :one
INSERT INTO changes (
  repository_id,
  name,
  description,
  author_id,
  depth
)
VALUES (
  $1,  -- repository_id
  $2,  -- name
  $3,  -- description
  $4,  -- author_id
  0
)
RETURNING id AS change_id;

-- name: SetParent :exec
INSERT INTO change_relations (
  change_id,
  parent_id
)
VALUES (
  $1,  -- child change_id
  $2   -- parent_id
)
ON CONFLICT (change_id, parent_id) DO NOTHING;

-- name: SetDepthFromParent :exec
UPDATE changes AS child
SET depth      = GREATEST(parent.depth + 1, child.depth),
    updated_at = CURRENT_TIMESTAMP
FROM changes AS parent
WHERE child.id   = $1  -- child change_id
  AND parent.id  = $2; -- parent change_id

-- name: GetAllChangeRelations :many
SELECT
  child.id    AS child_id,
  child.name  AS child_name,
  parent.id   AS parent_id,
  COALESCE(parent.name, '~') AS parent_name
FROM change_relations cr
JOIN changes child
  ON child.id = cr.change_id
LEFT JOIN changes parent
  ON parent.id = cr.parent_id
WHERE child.repository_id = $1;

-- name: GetNewestChanges :many
SELECT
  id,
  name,
  description,
  created_at,
  updated_at,
  get_unique_prefix(id) AS unique_prefix
FROM changes
WHERE repository_id = $1
ORDER BY updated_at DESC
LIMIT $2;

-- name: GetChangeRelationsForChanges :many
SELECT
  child.id    AS child_id,
  child.name  AS child_name,
  parent.id   AS parent_id,
  COALESCE(parent.name, '~') AS parent_name
FROM change_relations cr
JOIN changes child
  ON child.id = cr.change_id
LEFT JOIN changes parent
  ON parent.id = cr.parent_id
WHERE child.id = ANY(@change_ids::BIGINT[]);

-- name: GetChange :one
SELECT
  changes.*,
  get_unique_prefix(changes.id) AS unique_prefix
FROM changes
WHERE id = $1;

-- name: GetChangeName :one
SELECT name FROM changes WHERE id = $1;

-- name: GetRepositoryFilesForChangeId :many
SELECT DISTINCT f.*
FROM files f
JOIN change_files cf ON f.id = cf.file_id
WHERE cf.change_id = $1
ORDER BY f.name;

-- name: GetChangeDescription :one
SELECT description FROM changes WHERE id = $1;

-- name: GetChangeBookmarks :many
SELECT b.name
FROM bookmarks b
JOIN changes c ON c.id = b.change_id
WHERE c.id = $1;

-- name: IsChangeInConflict :one
SELECT EXISTS (
    SELECT 1 FROM change_files cf
    JOIN files f ON cf.file_id = f.id
    WHERE cf.change_id = $1 AND f.conflict = TRUE
);

-- name: GetConflictFilesForChange :many
SELECT f.name FROM change_files cf
JOIN files f ON cf.file_id = f.id
WHERE cf.change_id = $1 AND f.conflict = TRUE
ORDER BY f.name;

-- name: FindLCA :one
WITH RECURSIVE ancestors AS (
    -- Start with the two target changes
    SELECT
        c.id AS change_id,
        c.id AS origin_id,
        c.depth,
        0 AS distance
    FROM changes c
    WHERE c.id IN ($1, $2)

    UNION ALL

    -- Walk up the DAG via parents
    SELECT
        cr.parent_id AS change_id,
        a.origin_id,
        c.depth,
        a.distance + 1
    FROM ancestors a
    JOIN change_relations cr ON cr.change_id = a.change_id
    JOIN changes c ON c.id = cr.parent_id
    WHERE c.depth <= (
        -- Stop at the minimum depth of our starting nodes
        SELECT MAX(depth) FROM changes WHERE id IN ($1, $2)
    )
)
SELECT change_id AS lca_id
FROM ancestors
GROUP BY change_id
HAVING COUNT(DISTINCT origin_id) = 2  -- Reachable from both inputs
ORDER BY MAX(depth) DESC  -- Highest depth = closest to leaves
LIMIT 1;

-- name: GetThreeWayMergeFiles :many
WITH all_file_names AS (
    SELECT DISTINCT f.name
    FROM change_files cf
    JOIN files f ON cf.file_id = f.id
    WHERE cf.change_id IN (@lca, @a, @b)
),
lca_files AS (
    SELECT DISTINCT ON (f.name) f.id, f.name, f.executable, f.content_hash
    FROM change_files cf
    JOIN files f ON cf.file_id = f.id
    WHERE cf.change_id = @lca
    ORDER BY f.name, f.id
),
a_files AS (
    SELECT DISTINCT ON (f.name) f.id, f.name, f.executable, f.content_hash
    FROM change_files cf
    JOIN files f ON cf.file_id = f.id
    WHERE cf.change_id = @a
    ORDER BY f.name, f.id
),
b_files AS (
    SELECT DISTINCT ON (f.name) f.id, f.name, f.executable, f.content_hash
    FROM change_files cf
    JOIN files f ON cf.file_id = f.id
    WHERE cf.change_id = @b
    ORDER BY f.name, f.id
)
SELECT
    afn.name as file_name,
    lca.id as lca_file_id,
    lca.executable as lca_executable,
    lca.content_hash as lca_content_hash,
    a.id as a_file_id,
    a.executable as a_executable,
    a.content_hash as a_content_hash,
    b.id as b_file_id,
    b.executable as b_executable,
    b.content_hash as b_content_hash
FROM all_file_names afn
LEFT JOIN lca_files lca ON afn.name = lca.name
LEFT JOIN a_files a ON afn.name = a.name
LEFT JOIN b_files b ON afn.name = b.name
ORDER BY afn.name;

-- name: CopyChangeFiles :exec
INSERT INTO change_files (
  change_id,
  file_id
)
SELECT
  @target_change,
  src.file_id
FROM
  change_files AS src
WHERE
  src.change_id = @source_change
ON CONFLICT (change_id, file_id) DO NOTHING;

-- name: GetChangeFiles :many
SELECT f.id, f.content_hash
FROM files f
JOIN change_files cf ON f.id = cf.file_id
WHERE cf.change_id = $1;

-- name: ClearChangeFiles :exec
DELETE FROM change_files WHERE change_id = $1;

-- name: FindChangeByNameExact :one
SELECT id FROM changes WHERE repository_id = $1 AND name = @revision::text;

-- name: FindChangeByNameFuzzy :one
WITH matches AS (
    -- First priority: exact bookmark match
    SELECT b.change_id AS id, 1 AS priority
    FROM bookmarks b
    WHERE b.repository_id = @repository
      AND b.name = @revision::text

    UNION ALL

    -- Second priority: exact change name match
    SELECT c.id, 2 AS priority
    FROM changes c
    WHERE c.repository_id = @repository
      AND c.name = @revision::text

    UNION ALL

    -- Third priority: change name prefix match
    SELECT c.id, 3 AS priority
    FROM changes c
    WHERE c.repository_id = @repository
      AND c.name LIKE @revision::text || '%'
      AND c.name != @revision::text  -- Exclude exact matches (already covered above)
)
SELECT id FROM matches
ORDER BY priority ASC
LIMIT 1;

-- name: GetUniquePrefix :one
SELECT get_unique_prefix($1::BIGINT) AS unique_prefix;

-- name: UpdateChangeTimestamp :exec
UPDATE changes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1;

-- name: SetChangeDescription :exec
UPDATE changes SET description = @description::text WHERE id = $1;

-- name: AddFileToChange :exec
WITH ins AS (
  INSERT INTO files (name, executable, content_hash, conflict)
  VALUES ($2, $3, $4, $5)
  ON CONFLICT (name, executable, content_hash) DO UPDATE SET conflict = $5
  RETURNING id
)
INSERT INTO change_files (change_id, file_id)
SELECT $1, COALESCE((SELECT id FROM ins), (SELECT id FROM files WHERE files.name = $2 AND files.executable = $3 AND files.content_hash = $4))
ON CONFLICT (change_id, file_id) DO NOTHING;

-- name: GetPublicRepositoryBookmarks :many
SELECT b.name
FROM bookmarks b
JOIN changes c ON b.change_id = c.id
JOIN repositories r ON c.repository_id = r.id
WHERE r.name = $1 AND r.public = TRUE
ORDER BY c.updated_at DESC;

-- name: GetPublicBookmarkTimeStamp :one
SELECT c.updated_at
FROM bookmarks b
JOIN changes c ON b.change_id = c.id
JOIN repositories r ON c.repository_id = r.id
WHERE r.name = $1 AND r.public = TRUE AND b.name = $2;

-- name: GetPublicGoModByBookmark :one
SELECT f.content_hash
FROM files f
JOIN change_files cf ON f.id = cf.file_id
JOIN changes c ON cf.change_id = c.id
JOIN bookmarks b ON c.id = b.change_id
JOIN repositories r ON c.repository_id = r.id
WHERE r.name = @repository AND r.public = TRUE AND b.name = @bookmark AND f.name = 'go.mod'
LIMIT 1;

-- name: GetPublicFileHashesByBookmark :many
SELECT f.name, f.content_hash
FROM files f
JOIN change_files cf ON f.id = cf.file_id
JOIN changes c ON cf.change_id = c.id
JOIN bookmarks b ON c.id = b.change_id
JOIN repositories r ON c.repository_id = r.id
WHERE r.name = @repository AND r.public = TRUE AND b.name = @bookmark
ORDER BY f.name;

-- name: SetBookmark :exec
INSERT INTO bookmarks (repository_id, name, change_id)
VALUES ($1, $2, $3)
ON CONFLICT (repository_id, name) DO UPDATE SET change_id = $3;

-- name: GetBookmark :one
SELECT change_id
FROM bookmarks
WHERE repository_id = $1 AND name = $2;

-- name: GetBookmarks :many
SELECT b.name AS bookmark, b.change_id, c.name AS change_name, c.updated_at
FROM bookmarks b
JOIN changes c ON c.id = b.change_id
WHERE b.repository_id = $1;

-- name: RemoveBookmark :exec
DELETE FROM bookmarks
WHERE repository_id = $1 AND name = $2;

-- name: GetCIConfigFiles :many
SELECT f.name, f.content_hash
FROM files f
JOIN change_files cf ON f.id = cf.file_id
WHERE cf.change_id = $1 AND f.name LIKE '.pogo/ci/%';

-- name: GetChangeChildren :many
SELECT c.id, c.name
FROM changes c
JOIN change_relations cr ON c.id = cr.change_id
WHERE cr.parent_id = $1;

-- name: GetChangeParents :many
SELECT c.id, c.name
FROM changes c
JOIN change_relations cr ON c.id = cr.parent_id
WHERE cr.change_id = $1;

-- name: DeleteChange :exec
DELETE FROM changes WHERE id = $1;

-- name: UpdateChildrenParents :exec
INSERT INTO change_relations (change_id, parent_id)
SELECT child.change_id, parent.parent_id
FROM change_relations child
CROSS JOIN change_relations parent
WHERE child.parent_id = $1 AND parent.change_id = $1
ON CONFLICT (change_id, parent_id) DO NOTHING;

-- name: RemoveChangeRelations :exec
DELETE FROM change_relations WHERE change_id = $1 OR parent_id = $1;

-- name: GetAllDescendants :many
WITH RECURSIVE descendants AS (
    -- Start with the target change
    SELECT c.id AS change_id, c.name AS change_name, c.depth AS change_depth
    FROM changes c
    WHERE c.id = $1

    UNION ALL

    -- Recursively find all children
    SELECT child.id AS change_id, child.name AS change_name, child.depth AS change_depth
    FROM changes child
    JOIN change_relations cr ON child.id = cr.change_id
    JOIN descendants parent ON cr.parent_id = parent.change_id
)
SELECT change_id, change_name, change_depth
FROM descendants
WHERE change_id != $1  -- Exclude the root change itself
ORDER BY change_depth DESC;  -- Delete deepest children first

-- name: UpdateRepositoryName :exec
UPDATE repositories SET name = $2 WHERE id = $1;

-- name: UpdateRepositoryVisibility :exec
UPDATE repositories SET public = $2 WHERE id = $1;

-- name: GrantRepositoryAccessByUsername :exec
INSERT INTO repository_access (repository_id, user_id)
SELECT $1, u.id
FROM users u
WHERE u.username = $2
ON CONFLICT (repository_id, user_id) DO NOTHING;

-- name: RevokeRepositoryAccessByUsername :exec
DELETE FROM repository_access
WHERE repository_id = $1 
  AND user_id = (SELECT id FROM users WHERE username = $2);

-- name: GetReachableFileHashes :many
SELECT DISTINCT f.content_hash
FROM files f
JOIN change_files cf ON f.id = cf.file_id
JOIN changes c ON cf.change_id = c.id
JOIN repositories r ON c.repository_id = r.id
ORDER BY f.content_hash;

-- name: GetUnreachableFiles :many
SELECT f.id, f.content_hash
FROM files f
WHERE NOT EXISTS (
    SELECT 1 FROM change_files cf
    WHERE cf.file_id = f.id
);

-- name: DeleteUnreachableFiles :exec
DELETE FROM files
WHERE NOT EXISTS (
    SELECT 1 FROM change_files cf
    WHERE cf.file_id = files.id
);

-- name: GetAllFileHashes :many
SELECT DISTINCT content_hash FROM files ORDER BY content_hash;

-- name: CountFiles :one
SELECT COUNT(DISTINCT content_hash) AS count FROM files;

-- name: GetOrphanedFileIds :many
SELECT f.id, f.content_hash
FROM files f
WHERE f.id = ANY(@file_ids::BIGINT[])
AND NOT EXISTS (
    SELECT 1 FROM change_files cf
    WHERE cf.file_id = f.id
);

-- name: DeleteFilesByIds :exec
DELETE FROM files
WHERE id = ANY(@file_ids::BIGINT[]);

-- name: CheckFileHashExists :one
SELECT EXISTS(SELECT 1 FROM files WHERE content_hash = $1) AS exists;

-- name: CheckMultipleFileHashesExist :many
SELECT input.hash AS content_hash
FROM (SELECT unnest(@hashes::BYTEA[]) AS hash) AS input
WHERE EXISTS (SELECT 1 FROM files WHERE content_hash = input.hash);

-- name: IsReadonly :one
SELECT EXISTS (
    -- Change has bookmarks pointing to it
    SELECT 1 FROM bookmarks b WHERE b.change_id = @change_id
    UNION
    -- Change has children
    SELECT 1 FROM change_relations cr WHERE cr.parent_id = @change_id
    UNION
    -- Change author is someone else
    SELECT 1 FROM changes c WHERE c.id = @change_id AND c.author_id != @user_id
) AS is_readonly;

-- name: GetRepositoryRootChange :one
SELECT c.id, c.name
FROM changes c
WHERE c.repository_id = $1
  AND NOT EXISTS (
    SELECT 1 FROM change_relations cr WHERE cr.change_id = c.id
  )
LIMIT 1;

-- name: CreateInvite :one
INSERT INTO invites (token, created_by_user_id, expires_at)
VALUES ($1, $2, $3) RETURNING id;

-- name: GetInviteByToken :one
SELECT * FROM invites WHERE token = $1;

-- name: UseInvite :exec
UPDATE invites SET used_at = CURRENT_TIMESTAMP, used_by_user_id = $2
WHERE token = $1 AND used_at IS NULL;

-- name: GetActiveInvitesByUser :many
SELECT * FROM invites 
WHERE created_by_user_id = $1 
  AND used_at IS NULL 
  AND expires_at > CURRENT_TIMESTAMP
ORDER BY created_at DESC;

-- name: GetAllInvitesByUser :many
SELECT i.*, u.username as used_by_username
FROM invites i
LEFT JOIN users u ON i.used_by_user_id = u.id
WHERE i.created_by_user_id = $1
ORDER BY i.created_at DESC;

-- name: DeleteExpiredInvites :exec
DELETE FROM invites WHERE expires_at < CURRENT_TIMESTAMP;

-- name: RevokeInvite :exec
DELETE FROM invites WHERE token = $1 AND created_by_user_id = $2 AND used_at IS NULL;

-- name: SetSecret :exec
INSERT INTO secrets (repository_id, key, value)
VALUES ($1, $2, $3)
ON CONFLICT (repository_id, key) DO UPDATE SET value = $3;

-- name: GetSecret :one
SELECT value FROM secrets WHERE repository_id = $1 AND key = $2;

-- name: GetAllSecrets :many
SELECT key, value FROM secrets WHERE repository_id = $1 ORDER BY key;

-- name: DeleteSecret :exec
DELETE FROM secrets WHERE repository_id = $1 AND key = $2;

-- name: CreateCIRun :one
INSERT INTO ci_runs (
  repository_id,
  config_filename,
  event_type,
  rev,
  pattern,
  reason,
  task_type,
  status_code,
  success,
  started_at,
  finished_at,
  log
) VALUES (
  @repository_id,
  @config_filename,
  @event_type,
  @rev,
  @pattern,
  @reason,
  @task_type,
  @status_code,
  @success,
  @started_at,
  @finished_at,
  @log
) RETURNING id;

-- name: ListCIRuns :many
SELECT
  id,
  repository_id,
  config_filename,
  event_type,
  rev,
  pattern,
  reason,
  task_type,
  status_code,
  success,
  started_at,
  finished_at
FROM ci_runs
WHERE repository_id = $1
ORDER BY started_at DESC;

-- name: GetCIRun :one
SELECT
  id,
  repository_id,
  config_filename,
  event_type,
  rev,
  pattern,
  reason,
  task_type,
  status_code,
  success,
  started_at,
  finished_at,
  log
FROM ci_runs
WHERE repository_id = $1 AND id = $2;

-- name: UpdateCIRun :exec
UPDATE ci_runs
SET
  status_code = @status_code,
  success = @success,
  finished_at = @finished_at,
  log = @log
WHERE id = @id;

-- name: DeleteExpiredCIRuns :one
WITH deleted AS (
  DELETE FROM ci_runs WHERE started_at < $1 RETURNING 1
)
SELECT COUNT(*) FROM deleted;

-- name: GetFilesForChange :many
SELECT f.name, f.executable, f.content_hash
FROM files f
JOIN change_files cf ON f.id = cf.file_id
WHERE cf.change_id = $1
ORDER BY f.name;

-- name: EnsureCIUser :exec
INSERT INTO users (id, username) VALUES (-1, 'CI') ON CONFLICT (id) DO NOTHING;
