-- Derived project discovery projection. Project/Folder remain authoritative;
-- triggers prevent index drift through ordinary database mutations.
CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE TYPE "FolderKind" AS ENUM ('REGULAR', 'SYSTEM_INBOX');
ALTER TABLE "Folder" ADD COLUMN "kind" "FolderKind" NOT NULL DEFAULT 'REGULAR';

DO $$
BEGIN
  IF EXISTS (
    SELECT 1 FROM "Folder"
    WHERE "parentId" IS NULL AND "name" = 'Inbox' AND "archivedAt" IS NULL
    GROUP BY "organizationId" HAVING count(*) > 1
  ) THEN
    RAISE EXCEPTION 'cannot identify system Inbox: organization has multiple active root Inbox folders';
  END IF;
END $$;

UPDATE "Folder" f SET "kind" = 'SYSTEM_INBOX'
WHERE f."parentId" IS NULL AND f."name" = 'Inbox' AND f."archivedAt" IS NULL;

INSERT INTO "Folder" ("id", "organizationId", "parentId", "name", "kind", "sortKey", "createdAt", "updatedAt")
SELECT o."id" || ':system-inbox', o."id", NULL, 'Inbox', 'SYSTEM_INBOX', '000000', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
FROM "Organization" o
WHERE NOT EXISTS (
  SELECT 1 FROM "Folder" f
  WHERE f."organizationId" = o."id" AND f."kind" = 'SYSTEM_INBOX' AND f."archivedAt" IS NULL
);

CREATE UNIQUE INDEX "Folder_one_active_system_inbox_per_org"
  ON "Folder"("organizationId") WHERE "kind" = 'SYSTEM_INBOX' AND "archivedAt" IS NULL;

CREATE OR REPLACE FUNCTION cph_protect_system_inbox() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  IF TG_OP = 'UPDATE' AND OLD."kind" = 'SYSTEM_INBOX' AND (
    NEW."id" IS DISTINCT FROM OLD."id" OR
    NEW."organizationId" IS DISTINCT FROM OLD."organizationId" OR
    NEW."kind" IS DISTINCT FROM OLD."kind" OR
    NEW."name" IS DISTINCT FROM OLD."name" OR
    NEW."parentId" IS DISTINCT FROM OLD."parentId" OR
    NEW."archivedAt" IS DISTINCT FROM OLD."archivedAt"
  ) THEN
    RAISE EXCEPTION 'system Inbox identity cannot be changed';
  END IF;
  IF TG_OP IN ('INSERT', 'UPDATE') AND NEW."kind" = 'SYSTEM_INBOX' AND (
    NEW."name" <> 'Inbox' OR NEW."parentId" IS NOT NULL OR NEW."archivedAt" IS NOT NULL
  ) THEN
    RAISE EXCEPTION 'system Inbox must be an active root folder named Inbox';
  END IF;
  IF TG_OP = 'DELETE' AND OLD."kind" = 'SYSTEM_INBOX' AND EXISTS (
    SELECT 1 FROM "Organization" WHERE "id" = OLD."organizationId"
  ) THEN
    RAISE EXCEPTION 'system Inbox cannot be deleted while its organization exists';
  END IF;
  RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
END;
$$;

CREATE TRIGGER cph_protect_system_inbox
BEFORE INSERT OR UPDATE OR DELETE ON "Folder"
FOR EACH ROW EXECUTE FUNCTION cph_protect_system_inbox();

ALTER TABLE "Project" ADD COLUMN "code" TEXT;

CREATE TABLE "ProjectSearchDocument" (
    "projectId" TEXT NOT NULL,
    "organizationId" TEXT NOT NULL,
    "name" TEXT NOT NULL,
    "code" TEXT,
    "normalizedCode" TEXT NOT NULL,
    "normalizedName" TEXT NOT NULL,
    "breadcrumb" TEXT NOT NULL,
    "normalizedBreadcrumb" TEXT NOT NULL,
    "normalizedSearchText" TEXT NOT NULL,
    "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT "ProjectSearchDocument_pkey" PRIMARY KEY ("projectId")
);

CREATE INDEX "ProjectSearchDocument_organizationId_idx" ON "ProjectSearchDocument"("organizationId");
CREATE INDEX "ProjectSearchDocument_normalizedName_trgm_idx"
  ON "ProjectSearchDocument" USING GIN ("normalizedName" gin_trgm_ops);
CREATE INDEX "ProjectSearchDocument_normalizedCode_trgm_idx"
  ON "ProjectSearchDocument" USING GIN ("normalizedCode" gin_trgm_ops);
CREATE INDEX "ProjectSearchDocument_normalizedBreadcrumb_trgm_idx"
  ON "ProjectSearchDocument" USING GIN ("normalizedBreadcrumb" gin_trgm_ops);
CREATE INDEX "ProjectSearchDocument_normalizedSearchText_trgm_idx"
  ON "ProjectSearchDocument" USING GIN ("normalizedSearchText" gin_trgm_ops);

ALTER TABLE "ProjectSearchDocument" ADD CONSTRAINT "ProjectSearchDocument_projectId_fkey"
  FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProjectSearchDocument" ADD CONSTRAINT "ProjectSearchDocument_organizationId_fkey"
  FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;

CREATE OR REPLACE FUNCTION cph_project_search_normalize(value TEXT) RETURNS TEXT
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$
  SELECT lower(regexp_replace(normalize(value, NFKC), '[[:space:]_.:/\\-]+', '', 'g'))
$$;

CREATE OR REPLACE FUNCTION cph_folder_breadcrumb(folder_id TEXT) RETURNS TEXT
LANGUAGE sql STABLE PARALLEL SAFE AS $$
  WITH RECURSIVE ancestors AS (
    SELECT f."id", f."parentId", f."name", f."kind", 0 AS depth
    FROM "Folder" f
    WHERE f."id" = folder_id
    UNION ALL
    SELECT parent."id", parent."parentId", parent."name", parent."kind", child.depth + 1
    FROM "Folder" parent
    JOIN ancestors child ON parent."id" = child."parentId"
  )
  SELECT CASE
    WHEN count(*) = 1 AND bool_and("kind" = 'SYSTEM_INBOX') THEN '未分类'
    ELSE coalesce(string_agg("name", ' / ' ORDER BY depth DESC), '')
  END
  FROM ancestors
$$;

CREATE OR REPLACE FUNCTION cph_project_search_code(explicit_code TEXT, project_name TEXT) RETURNS TEXT
LANGUAGE sql IMMUTABLE PARALLEL SAFE AS $$
  SELECT CASE
    WHEN explicit_code IS NOT NULL AND btrim(explicit_code) <> ''
      THEN cph_project_search_normalize(explicit_code)
    ELSE coalesce(substring(cph_project_search_normalize(project_name) FROM '^[a-z]+[0-9]+'), '')
  END
$$;

CREATE OR REPLACE FUNCTION cph_refresh_project_search_document(target_project_id TEXT) RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
  INSERT INTO "ProjectSearchDocument" (
    "projectId", "organizationId", "name", "code", "normalizedCode", "normalizedName",
    "breadcrumb", "normalizedBreadcrumb", "normalizedSearchText", "updatedAt"
  )
  SELECT p."id", p."organizationId", p."name", p."code",
    cph_project_search_code(p."code", p."name"),
    cph_project_search_normalize(p."name"),
    cph_folder_breadcrumb(p."folderId"),
    cph_project_search_normalize(cph_folder_breadcrumb(p."folderId")),
    cph_project_search_normalize(cph_folder_breadcrumb(p."folderId") || ' ' || p."name"),
    CURRENT_TIMESTAMP
  FROM "Project" p WHERE p."id" = target_project_id
  ON CONFLICT ("projectId") DO UPDATE SET
    "organizationId" = EXCLUDED."organizationId",
    "name" = EXCLUDED."name",
    "code" = EXCLUDED."code",
    "normalizedCode" = EXCLUDED."normalizedCode",
    "normalizedName" = EXCLUDED."normalizedName",
    "breadcrumb" = EXCLUDED."breadcrumb",
    "normalizedBreadcrumb" = EXCLUDED."normalizedBreadcrumb",
    "normalizedSearchText" = EXCLUDED."normalizedSearchText",
    "updatedAt" = CURRENT_TIMESTAMP;
END;
$$;

CREATE OR REPLACE FUNCTION cph_project_search_project_trigger() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  PERFORM cph_refresh_project_search_document(NEW."id");
  RETURN NEW;
END;
$$;

CREATE TRIGGER cph_project_search_project_changed
AFTER INSERT OR UPDATE OF "name", "code", "folderId", "organizationId", "archivedAt" ON "Project"
FOR EACH ROW EXECUTE FUNCTION cph_project_search_project_trigger();

CREATE OR REPLACE FUNCTION cph_project_search_folder_trigger() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE project_id TEXT;
BEGIN
  FOR project_id IN
    WITH RECURSIVE descendants AS (
      SELECT NEW."id"
      UNION ALL
      SELECT child."id" FROM "Folder" child
      JOIN descendants parent ON child."parentId" = parent."id"
    )
    SELECT p."id" FROM "Project" p
    WHERE p."folderId" IN (SELECT "id" FROM descendants)
  LOOP
    PERFORM cph_refresh_project_search_document(project_id);
  END LOOP;
  RETURN NEW;
END;
$$;

CREATE TRIGGER cph_project_search_folder_changed
AFTER UPDATE OF "name", "kind", "parentId", "archivedAt" ON "Folder"
FOR EACH ROW EXECUTE FUNCTION cph_project_search_folder_trigger();

SELECT cph_refresh_project_search_document("id") FROM "Project";
