-- =============================================================================
-- SÁNG KIẾN (INNOVATION APPLICATION) DATABASE SCHEMA
-- PostgreSQL 14+
--
-- Domain: Manage innovation applications at ĐHYD TP.HCM (Vietnamese medical
-- university). Supports the full lifecycle: draft → submit → evaluate → approve.
--
-- Design principles:
--   - 3NF for entities, JSONB for semi-structured/optional narrative
--   - Soft delete (deleted_at) — legal/audit requires historical retention
--   - State machine on applications.status enforced by trigger
--   - Full audit_log via trigger on all CUD operations
--   - Contribution % sums to 100 enforced by DEFERRABLE trigger
-- =============================================================================

CREATE EXTENSION IF NOT EXISTS pg_trgm;       -- fuzzy matching
CREATE EXTENSION IF NOT EXISTS unaccent;      -- Vietnamese diacritics in search

-- Convenience: updated_at auto-maintenance
CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS TRIGGER AS $$
BEGIN NEW.updated_at := NOW(); RETURN NEW; END;
$$ LANGUAGE plpgsql;


-- =============================================================================
-- REFERENCE: UNITS (departments, faculties, centers)
-- =============================================================================
CREATE TABLE units (
    unit_id        SERIAL PRIMARY KEY,
    code           VARCHAR(32) UNIQUE NOT NULL,
    name           VARCHAR(255) NOT NULL,                         -- full Vietnamese name
    parent_unit_id INT REFERENCES units(unit_id) ON DELETE SET NULL,
    type           VARCHAR(32) NOT NULL
                   CHECK (type IN ('TRUONG','KHOA','PHONG','BO_MON','TRUNG_TAM','KHAC')),
    is_active      BOOLEAN NOT NULL DEFAULT TRUE,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at     TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TRIGGER trg_units_touch BEFORE UPDATE ON units
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();


-- =============================================================================
-- USERS (unified: authors, evaluators, admins — a user can wear many hats)
-- =============================================================================
CREATE TABLE users (
    user_id        SERIAL PRIMARY KEY,
    full_name      VARCHAR(255) NOT NULL,
    title          VARCHAR(64),            -- PGS.TS, TS., GS., CN., ThS.
    date_of_birth  DATE,
    email          VARCHAR(255) UNIQUE,
    phone          VARCHAR(32),
    id_number      VARCHAR(32) UNIQUE,     -- CCCD / hộ chiếu
    unit_id        INT REFERENCES units(unit_id) ON DELETE SET NULL,
    position       VARCHAR(255),           -- chức danh: Trưởng phòng, GV cao cấp
    qualification  VARCHAR(64),            -- trình độ: Tiến sĩ, Thạc sĩ, Cử nhân
    user_type      VARCHAR(32) NOT NULL DEFAULT 'AUTHOR'
                   CHECK (user_type IN ('AUTHOR','COUNCIL','ADMIN','STUDENT','EXTERNAL')),
    is_active      BOOLEAN NOT NULL DEFAULT TRUE,
    deleted_at     TIMESTAMPTZ,            -- soft delete
    created_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at     TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_unit ON users(unit_id);
CREATE INDEX idx_users_active ON users(is_active) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_name_trgm ON users USING GIN (full_name gin_trgm_ops);
CREATE TRIGGER trg_users_touch BEFORE UPDATE ON users
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();


-- =============================================================================
-- APPLICATIONS (sáng kiến) — the core entity
-- =============================================================================
CREATE TABLE applications (
    application_id        SERIAL PRIMARY KEY,
    code                  VARCHAR(32) UNIQUE NOT NULL,  -- e.g., 'SK-2025-001'
    title                 TEXT NOT NULL,
    title_en              TEXT,
    registration_year     INT NOT NULL CHECK (registration_year BETWEEN 2000 AND 2100),
    field_of_application  TEXT,                         -- lĩnh vực áp dụng

    -- Workflow state (enforced by trigger below)
    status                VARCHAR(32) NOT NULL DEFAULT 'DRAFT'
                          CHECK (status IN (
                            'DRAFT','SUBMITTED','UNDER_REVIEW',
                            'EVALUATED','APPROVED','REJECTED','WITHDRAWN'
                          )),

    -- Mẫu 01 narrative (long text)
    introduction          TEXT,         -- 1. Mở đầu
    current_state         TEXT,         -- 4.1 Tình trạng đã biết
    purpose               TEXT,         -- Mục đích
    implementation_steps  TEXT,         -- Các bước thực hiện
    required_conditions   TEXT,         -- Điều kiện cần thiết
    results_achieved      TEXT,         -- Kết quả thu được
    novelty_description   TEXT,         -- Tính mới
    confidential_info     TEXT,         -- Thông tin cần bảo mật

    -- 10 effectiveness sub-fields (all optional narrative) → JSONB
    effectiveness         JSONB NOT NULL DEFAULT '{}'::jsonb,
    -- Shape: { "economic":"...", "teaching":"...", "productivity":"...",
    --          "work_efficiency":"...", "quality":"...", "cost_reduction":"...",
    --          "environment":"...", "health":"...", "safety":"...", "awareness":"..." }

    -- Mẫu 02 fields
    owner_org             VARCHAR(255),       -- chủ đầu tư
    first_applied_date    DATE,               -- ngày áp dụng lần đầu
    content_summary       TEXT,               -- nội dung sáng kiến (short)
    author_assessment     TEXT,               -- đánh giá theo tác giả
    org_assessment        TEXT,               -- đánh giá theo tổ chức

    -- Mẫu 02 classification (mutually exclusive in form, but stored as flags)
    is_technical_solution     BOOLEAN NOT NULL DEFAULT FALSE,
    is_from_research_article  BOOLEAN NOT NULL DEFAULT FALSE,
    is_from_book_material     BOOLEAN NOT NULL DEFAULT FALSE,
    CONSTRAINT chk_exactly_one_classification CHECK (
        status = 'DRAFT' OR
        (is_technical_solution::int + is_from_research_article::int + is_from_book_material::int) = 1
    ),

    -- Workflow timestamps
    submitted_at          TIMESTAMPTZ,
    decided_at            TIMESTAMPTZ,

    primary_unit_id       INT REFERENCES units(unit_id),
    created_by            INT REFERENCES users(user_id),
    deleted_at            TIMESTAMPTZ,        -- soft delete
    created_at            TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at            TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_apps_status ON applications(status) WHERE deleted_at IS NULL;
CREATE INDEX idx_apps_year ON applications(registration_year);
CREATE INDEX idx_apps_unit ON applications(primary_unit_id);
CREATE INDEX idx_apps_title_trgm ON applications USING GIN (title gin_trgm_ops);
CREATE INDEX idx_apps_fts ON applications USING GIN (
    to_tsvector('simple',
        coalesce(title,'') || ' ' ||
        coalesce(introduction,'') || ' ' ||
        coalesce(novelty_description,'')
    )
);
CREATE INDEX idx_apps_effectiveness ON applications USING GIN (effectiveness);
CREATE TRIGGER trg_apps_touch BEFORE UPDATE ON applications
    FOR EACH ROW EXECUTE FUNCTION touch_updated_at();


-- =============================================================================
-- APPLICATION_AUTHORS (M:N with contribution %)
-- =============================================================================
CREATE TABLE application_authors (
    application_id   INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
    user_id          INT NOT NULL REFERENCES users(user_id),
    contribution_pct NUMERIC(5,2) NOT NULL CHECK (contribution_pct > 0 AND contribution_pct <= 100),
    role             VARCHAR(32) NOT NULL DEFAULT 'CO_AUTHOR'
                     CHECK (role IN ('PRIMARY','CO_AUTHOR')),
    display_order    INT NOT NULL DEFAULT 0,
    PRIMARY KEY (application_id, user_id)
);
CREATE INDEX idx_app_authors_user ON application_authors(user_id);

-- At most one PRIMARY author per application
CREATE UNIQUE INDEX uq_primary_per_app
    ON application_authors(application_id) WHERE role = 'PRIMARY';

-- Deferrable check: contribution % must total 100 per application
CREATE OR REPLACE FUNCTION check_contribution_total() RETURNS TRIGGER AS $$
DECLARE v_total NUMERIC; v_app INT;
BEGIN
    v_app := COALESCE(NEW.application_id, OLD.application_id);
    SELECT COALESCE(SUM(contribution_pct),0) INTO v_total
      FROM application_authors WHERE application_id = v_app;
    -- Only enforce when application has left DRAFT
    IF (SELECT status FROM applications WHERE application_id = v_app) <> 'DRAFT'
       AND v_total <> 100 THEN
        RAISE EXCEPTION 'Contribution % for application % must sum to 100 (got %)',
            '%', v_app, v_total;
    END IF;
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE CONSTRAINT TRIGGER trg_contribution_total
    AFTER INSERT OR UPDATE OR DELETE ON application_authors
    DEFERRABLE INITIALLY DEFERRED
    FOR EACH ROW EXECUTE FUNCTION check_contribution_total();


-- =============================================================================
-- ORGS that tested / adopted the innovation (Mẫu 01 inner table)
-- =============================================================================
CREATE TABLE application_adopters (
    adopter_id      SERIAL PRIMARY KEY,
    application_id  INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
    display_order   INT NOT NULL DEFAULT 0,
    org_name        VARCHAR(255) NOT NULL,
    address         TEXT,
    field           TEXT
);
CREATE INDEX idx_adopters_app ON application_adopters(application_id);


-- =============================================================================
-- PARTICIPANTS in first application (Mẫu 02 inner table)
-- =============================================================================
CREATE TABLE application_participants (
    participant_id  SERIAL PRIMARY KEY,
    application_id  INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
    user_id         INT REFERENCES users(user_id),        -- optional link
    display_order   INT NOT NULL DEFAULT 0,
    full_name       VARCHAR(255) NOT NULL,
    date_of_birth   DATE,
    work_unit       VARCHAR(255),
    position        VARCHAR(255),
    qualification   VARCHAR(64),
    support_content TEXT
);
CREATE INDEX idx_participants_app ON application_participants(application_id);


-- =============================================================================
-- EVALUATIONS (Mẫu 04) — council members score applications
-- =============================================================================
CREATE TABLE evaluations (
    evaluation_id         SERIAL PRIMARY KEY,
    application_id        INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
    evaluator_id          INT NOT NULL REFERENCES users(user_id),

    novelty_comments      TEXT,
    novelty_score         INT NOT NULL DEFAULT 0
                          CHECK (novelty_score BETWEEN 0 AND 40),

    effectiveness_comments TEXT,
    effectiveness_score   INT NOT NULL DEFAULT 0
                          CHECK (effectiveness_score BETWEEN 0 AND 60),

    total_score           INT GENERATED ALWAYS AS (novelty_score + effectiveness_score) STORED,
    conclusion            TEXT,
    evaluated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    UNIQUE (application_id, evaluator_id)
);
CREATE INDEX idx_eval_app ON evaluations(application_id);
CREATE INDEX idx_eval_evaluator ON evaluations(evaluator_id);


-- =============================================================================
-- COMMITMENTS (Bản cam kết) — for paper-based innovations
-- =============================================================================
CREATE TABLE commitments (
    commitment_id             SERIAL PRIMARY KEY,
    application_id            INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
    user_id                   INT NOT NULL REFERENCES users(user_id),

    paper_title               TEXT,
    role_type                 VARCHAR(32) NOT NULL
                              CHECK (role_type IN ('PRIMARY_AUTHOR','CO_AUTHOR')),

    -- 5 commitment checkboxes
    is_legal_owner            BOOLEAN NOT NULL DEFAULT FALSE,
    is_authorized_by_owner    BOOLEAN NOT NULL DEFAULT FALSE,
    has_coauthor_consent      BOOLEAN NOT NULL DEFAULT FALSE,
    not_predatory_journal     BOOLEAN NOT NULL DEFAULT FALSE,
    complies_with_ip_law      BOOLEAN NOT NULL DEFAULT FALSE,

    signed_at                 TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    UNIQUE (application_id, user_id)
);
CREATE INDEX idx_commit_app ON commitments(application_id);


-- =============================================================================
-- ATTACHMENTS (uploaded files — figures, flowcharts, annexes)
-- =============================================================================
CREATE TABLE attachments (
    attachment_id   SERIAL PRIMARY KEY,
    application_id  INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
    file_name       VARCHAR(255) NOT NULL,
    file_path       TEXT NOT NULL,                 -- S3/MinIO key
    file_size       BIGINT,
    mime_type       VARCHAR(128),
    kind            VARCHAR(32)                    -- 'LUU_DO', 'PHU_LUC', 'KY_SO', 'KHAC'
                    CHECK (kind IS NULL OR kind IN ('LUU_DO','PHU_LUC','KY_SO','KHAC')),
    uploaded_by     INT REFERENCES users(user_id),
    uploaded_at     TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_attach_app ON attachments(application_id);


-- =============================================================================
-- AUDIT LOG — single table, populated by triggers on all CUD operations
-- =============================================================================
CREATE TABLE audit_log (
    log_id       BIGSERIAL PRIMARY KEY,
    table_name   VARCHAR(64) NOT NULL,
    record_id    TEXT NOT NULL,
    action       VARCHAR(16) NOT NULL CHECK (action IN ('INSERT','UPDATE','DELETE')),
    changed_by   INT,                   -- set from app via SET LOCAL my.user_id
    changed_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    old_data     JSONB,
    new_data     JSONB
);
CREATE INDEX idx_audit_table_record ON audit_log(table_name, record_id);
CREATE INDEX idx_audit_user_time ON audit_log(changed_by, changed_at DESC);

-- Generic audit trigger function
CREATE OR REPLACE FUNCTION audit_trigger() RETURNS TRIGGER AS $$
DECLARE
    v_user INT;
    v_pk   TEXT;
BEGIN
    -- Get user_id from session var if app sets it; else NULL
    BEGIN v_user := current_setting('my.user_id')::INT;
    EXCEPTION WHEN OTHERS THEN v_user := NULL; END;

    v_pk := COALESCE(
        (row_to_json(NEW)::jsonb->>TG_ARGV[0]),
        (row_to_json(OLD)::jsonb->>TG_ARGV[0])
    );

    INSERT INTO audit_log(table_name, record_id, action, changed_by, old_data, new_data)
    VALUES (
        TG_TABLE_NAME,
        v_pk,
        TG_OP,
        v_user,
        CASE WHEN TG_OP IN ('UPDATE','DELETE') THEN to_jsonb(OLD) END,
        CASE WHEN TG_OP IN ('INSERT','UPDATE') THEN to_jsonb(NEW) END
    );
    RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

-- Attach audit trigger to the important tables (pass PK column name as arg)
CREATE TRIGGER trg_audit_applications AFTER INSERT OR UPDATE OR DELETE ON applications
    FOR EACH ROW EXECUTE FUNCTION audit_trigger('application_id');
CREATE TRIGGER trg_audit_authors AFTER INSERT OR UPDATE OR DELETE ON application_authors
    FOR EACH ROW EXECUTE FUNCTION audit_trigger('application_id');
CREATE TRIGGER trg_audit_evaluations AFTER INSERT OR UPDATE OR DELETE ON evaluations
    FOR EACH ROW EXECUTE FUNCTION audit_trigger('evaluation_id');
CREATE TRIGGER trg_audit_commitments AFTER INSERT OR UPDATE OR DELETE ON commitments
    FOR EACH ROW EXECUTE FUNCTION audit_trigger('commitment_id');


-- =============================================================================
-- WORKFLOW STATE MACHINE ENFORCEMENT
-- =============================================================================
CREATE OR REPLACE FUNCTION enforce_application_transitions() RETURNS TRIGGER AS $$
DECLARE
    allowed BOOLEAN := FALSE;
BEGIN
    IF OLD.status = NEW.status THEN RETURN NEW; END IF;

    -- Allowed transitions
    allowed := CASE
      WHEN OLD.status = 'DRAFT'        AND NEW.status IN ('SUBMITTED','WITHDRAWN') THEN TRUE
      WHEN OLD.status = 'SUBMITTED'    AND NEW.status IN ('UNDER_REVIEW','WITHDRAWN','DRAFT') THEN TRUE
      WHEN OLD.status = 'UNDER_REVIEW' AND NEW.status IN ('EVALUATED','WITHDRAWN') THEN TRUE
      WHEN OLD.status = 'EVALUATED'    AND NEW.status IN ('APPROVED','REJECTED') THEN TRUE
      ELSE FALSE
    END;

    IF NOT allowed THEN
        RAISE EXCEPTION 'Invalid status transition: % → %', OLD.status, NEW.status;
    END IF;

    -- Auto-set timestamps
    IF NEW.status = 'SUBMITTED' AND OLD.status = 'DRAFT' THEN
        NEW.submitted_at := NOW();
    END IF;
    IF NEW.status IN ('APPROVED','REJECTED') THEN
        NEW.decided_at := NOW();
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_app_state_machine
    BEFORE UPDATE OF status ON applications
    FOR EACH ROW EXECUTE FUNCTION enforce_application_transitions();


-- =============================================================================
-- CONVENIENCE VIEWS
-- =============================================================================

-- Dashboard: applications with author names and current evaluation average
CREATE VIEW v_application_summary AS
SELECT
    a.application_id,
    a.code,
    a.title,
    a.status,
    a.registration_year,
    u.name AS primary_unit_name,
    (SELECT string_agg(usr.full_name, ', ' ORDER BY aa.display_order)
       FROM application_authors aa
       JOIN users usr ON usr.user_id = aa.user_id
      WHERE aa.application_id = a.application_id) AS author_names,
    (SELECT ROUND(AVG(total_score),2)
       FROM evaluations WHERE application_id = a.application_id) AS avg_score,
    (SELECT COUNT(*) FROM evaluations WHERE application_id = a.application_id) AS num_evaluations,
    a.submitted_at,
    a.decided_at
FROM applications a
LEFT JOIN units u ON u.unit_id = a.primary_unit_id
WHERE a.deleted_at IS NULL;

-- Materialized view: annual approval statistics (refresh nightly)
CREATE MATERIALIZED VIEW mv_annual_stats AS
SELECT
    registration_year,
    COUNT(*) FILTER (WHERE status = 'APPROVED') AS approved,
    COUNT(*) FILTER (WHERE status = 'REJECTED') AS rejected,
    COUNT(*) FILTER (WHERE status NOT IN ('APPROVED','REJECTED')) AS pending,
    COUNT(*) AS total
FROM applications
WHERE deleted_at IS NULL
GROUP BY registration_year;
CREATE UNIQUE INDEX ON mv_annual_stats(registration_year);
