-- ============================================================
--  IKnow / FINKI - Phase 7: advanced database development
--
--  Run after schema_creation.sql and data_load.sql:
--      schema_creation.sql  -> tables, enums
--      data_load.sql        -> sample data
--      advanced.sql         -> this file
--
--  Re-runnable: every object is created with OR REPLACE or dropped first.
-- ============================================================

SET search_path TO project;

-- ============================================================
--  1. CUSTOM DOMAINS
--     One place to define what a valid value looks like, reused by
--     every column of that kind.
-- ============================================================

DROP DOMAIN IF EXISTS email_address CASCADE;
CREATE DOMAIN email_address AS VARCHAR(150)
    CHECK (VALUE ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');

DROP DOMAIN IF EXISTS embg_number CASCADE;
CREATE DOMAIN embg_number AS VARCHAR(13)
    CHECK (VALUE ~ '^[0-9]{13}$');

DROP DOMAIN IF EXISTS student_index CASCADE;
CREATE DOMAIN student_index AS VARCHAR(20)
    CHECK (VALUE ~ '^[0-9]{6}$');

DROP DOMAIN IF EXISTS phone_number CASCADE;
CREATE DOMAIN phone_number AS VARCHAR(20)
    CHECK (VALUE ~ '^[0-9+][0-9 /-]{5,19}$');

DROP DOMAIN IF EXISTS money_amount CASCADE;
CREATE DOMAIN money_amount AS INTEGER
    CHECK (VALUE >= 0);

DROP DOMAIN IF EXISTS gpa_value CASCADE;
CREATE DOMAIN gpa_value AS REAL
    CHECK (VALUE >= 2.0 AND VALUE <= 5.0);

DROP DOMAIN IF EXISTS credit_points CASCADE;
CREATE DOMAIN credit_points AS INTEGER
    CHECK (VALUE > 0 AND VALUE <= 30);

ALTER TABLE users       ALTER COLUMN email           TYPE email_address;
ALTER TABLE users       ALTER COLUMN embg            TYPE embg_number;
ALTER TABLE users       ALTER COLUMN "index"         TYPE student_index;
ALTER TABLE contact     ALTER COLUMN microsoft_email TYPE email_address;
ALTER TABLE contact     ALTER COLUMN number          TYPE phone_number;
ALTER TABLE payment     ALTER COLUMN amount          TYPE money_amount;
ALTER TABLE documents   ALTER COLUMN cost            TYPE money_amount;
ALTER TABLE high_school ALTER COLUMN gpa             TYPE gpa_value;
ALTER TABLE subjects    ALTER COLUMN awarded_credits TYPE credit_points;

-- ============================================================
--  2. MULTI-TENANCY: several faculties in one database
-- ============================================================

CREATE TABLE IF NOT EXISTS faculty (
    id         INTEGER      GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    short_name VARCHAR(20)  NOT NULL UNIQUE,
    full_name  VARCHAR(200) NOT NULL,
    university VARCHAR(200) NOT NULL
);

INSERT INTO faculty (id, short_name, full_name, university)
VALUES (1, 'FINKI', 'Факултет за информатички науки и компјутерско инженерство',
        'Универзитет „Св. Кирил и Методиј“ - Скопје')
ON CONFLICT (short_name) DO NOTHING;

-- The tenant key goes on the root entities. Everything else inherits its
-- faculty through them, so the column is not repeated on every table.
ALTER TABLE users            ADD COLUMN IF NOT EXISTS faculty_id INTEGER NOT NULL DEFAULT 1 REFERENCES faculty (id);
ALTER TABLE major            ADD COLUMN IF NOT EXISTS faculty_id INTEGER NOT NULL DEFAULT 1 REFERENCES faculty (id);
ALTER TABLE subjects         ADD COLUMN IF NOT EXISTS faculty_id INTEGER NOT NULL DEFAULT 1 REFERENCES faculty (id);
ALTER TABLE active_semesters ADD COLUMN IF NOT EXISTS faculty_id INTEGER NOT NULL DEFAULT 1 REFERENCES faculty (id);
ALTER TABLE documents        ADD COLUMN IF NOT EXISTS faculty_id INTEGER NOT NULL DEFAULT 1 REFERENCES faculty (id);

-- subjects.code and subjects.name were globally unique; with several
-- faculties they only have to be unique inside one faculty.
ALTER TABLE subjects DROP CONSTRAINT IF EXISTS subjects_code_key;
ALTER TABLE subjects DROP CONSTRAINT IF EXISTS subjects_name_key;
DROP INDEX IF EXISTS subjects_faculty_code_key;
DROP INDEX IF EXISTS subjects_faculty_name_key;
CREATE UNIQUE INDEX subjects_faculty_code_key ON subjects (faculty_id, code);
CREATE UNIQUE INDEX subjects_faculty_name_key ON subjects (faculty_id, name);

-- The faculty of the current session. Returns NULL when nothing is set,
-- which the policies below read as "no tenant filter".
CREATE OR REPLACE FUNCTION current_faculty()
    RETURNS INTEGER
    LANGUAGE plpgsql STABLE
AS $$
BEGIN
    RETURN nullif(current_setting('app.current_faculty', TRUE), '')::INTEGER;
EXCEPTION
    WHEN others THEN RETURN NULL;
END;
$$;

CREATE OR REPLACE PROCEDURE set_current_faculty(p_faculty INTEGER)
    LANGUAGE plpgsql
AS $$
BEGIN
    IF p_faculty IS NOT NULL AND NOT EXISTS (SELECT 1 FROM faculty WHERE id = p_faculty) THEN
        RAISE EXCEPTION 'Faculty % does not exist', p_faculty;
    END IF;
    PERFORM set_config('app.current_faculty', coalesce(p_faculty::TEXT, ''), FALSE);
END;
$$;

-- Row level security. The table owner bypasses these policies unless
-- FORCE ROW LEVEL SECURITY is used, so in production the application
-- connects with a separate, non-owner role.
ALTER TABLE users            ENABLE ROW LEVEL SECURITY;
ALTER TABLE major            ENABLE ROW LEVEL SECURITY;
ALTER TABLE subjects         ENABLE ROW LEVEL SECURITY;
ALTER TABLE active_semesters ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents        ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS tenant_isolation ON users;
DROP POLICY IF EXISTS tenant_isolation ON major;
DROP POLICY IF EXISTS tenant_isolation ON subjects;
DROP POLICY IF EXISTS tenant_isolation ON active_semesters;
DROP POLICY IF EXISTS tenant_isolation ON documents;

CREATE POLICY tenant_isolation ON users
    USING (current_faculty() IS NULL OR faculty_id = current_faculty());
CREATE POLICY tenant_isolation ON major
    USING (current_faculty() IS NULL OR faculty_id = current_faculty());
CREATE POLICY tenant_isolation ON subjects
    USING (current_faculty() IS NULL OR faculty_id = current_faculty());
CREATE POLICY tenant_isolation ON active_semesters
    USING (current_faculty() IS NULL OR faculty_id = current_faculty());
CREATE POLICY tenant_isolation ON documents
    USING (current_faculty() IS NULL OR faculty_id = current_faculty());

-- A foreign key cannot express "these three rows must belong to the same
-- faculty", so it is enforced with a trigger.
CREATE OR REPLACE FUNCTION check_enrolment_same_faculty()
    RETURNS TRIGGER
    LANGUAGE plpgsql
AS $$
DECLARE
    f_student  INTEGER;
    f_major    INTEGER;
    f_semester INTEGER;
BEGIN
    SELECT faculty_id INTO f_student  FROM users            WHERE id = NEW.user_id;
    SELECT faculty_id INTO f_major    FROM major            WHERE id = NEW.major_id;
    SELECT faculty_id INTO f_semester FROM active_semesters WHERE id = NEW.semester_id;

    IF f_student IS DISTINCT FROM f_major OR f_student IS DISTINCT FROM f_semester THEN
        RAISE EXCEPTION
            'Cross-faculty enrolment: student belongs to faculty %, major to %, semester to %',
            f_student, f_major, f_semester;
    END IF;
    RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS trg_enrolment_same_faculty ON enrolled_semesters;
CREATE TRIGGER trg_enrolment_same_faculty
    BEFORE INSERT OR UPDATE ON enrolled_semesters
    FOR EACH ROW EXECUTE FUNCTION check_enrolment_same_faculty();

-- ============================================================
--  3. NOTIFICATIONS
-- ============================================================

CREATE TABLE IF NOT EXISTS notification (
    id         INTEGER      GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    user_id    INTEGER      NOT NULL REFERENCES users (id),
    kind       VARCHAR(40)  NOT NULL,
    message    TEXT         NOT NULL,
    is_read    BOOLEAN      NOT NULL DEFAULT FALSE,
    created_at TIMESTAMP    NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS notification_user_unread_idx
    ON notification (user_id) WHERE is_read = FALSE;

-- ============================================================
--  4. ENROLMENT RULES
-- ============================================================

-- 4a. A subject must be offered by the study programme of the enrolment.
CREATE OR REPLACE FUNCTION check_subject_in_major()
    RETURNS TRIGGER
    LANGUAGE plpgsql
AS $$
DECLARE
    v_major INTEGER;
BEGIN
    SELECT major_id INTO v_major FROM enrolled_semesters WHERE id = NEW.enrolled_semesters_id;

    IF NOT EXISTS (SELECT 1 FROM major_subjects
                    WHERE major_id = v_major AND subject_id = NEW.subjects_id) THEN
        RAISE EXCEPTION 'Subject % is not offered by study programme %',
            NEW.subjects_id, v_major;
    END IF;
    RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS trg_subject_in_major ON semesters_subjects;
CREATE TRIGGER trg_subject_in_major
    BEFORE INSERT OR UPDATE ON semesters_subjects
    FOR EACH ROW EXECUTE FUNCTION check_subject_in_major();

-- 4b. The professor must actually teach that subject in that semester.
CREATE OR REPLACE FUNCTION check_professor_teaches()
    RETURNS TRIGGER
    LANGUAGE plpgsql
AS $$
DECLARE
    v_semester INTEGER;
    v_role     user_role;
BEGIN
    SELECT semester_id INTO v_semester FROM enrolled_semesters WHERE id = NEW.enrolled_semesters_id;
    SELECT role        INTO v_role     FROM users              WHERE id = NEW.professor_id;

    IF v_role <> 'prof' THEN
        RAISE EXCEPTION 'User % is not a professor', NEW.professor_id;
    END IF;

    IF NOT EXISTS (SELECT 1 FROM professour_subjects
                    WHERE prof_id = NEW.professor_id
                      AND subject_id = NEW.subjects_id
                      AND active_semester_id = v_semester) THEN
        RAISE EXCEPTION 'Professor % does not teach subject % in semester %',
            NEW.professor_id, NEW.subjects_id, v_semester;
    END IF;
    RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS trg_professor_teaches ON semesters_subjects;
CREATE TRIGGER trg_professor_teaches
    BEFORE INSERT OR UPDATE ON semesters_subjects
    FOR EACH ROW EXECUTE FUNCTION check_professor_teaches();

-- 4c. Prerequisites must already be passed.
CREATE OR REPLACE FUNCTION check_prerequisites_passed()
    RETURNS TRIGGER
    LANGUAGE plpgsql
AS $$
DECLARE
    v_user    INTEGER;
    v_missing TEXT;
BEGIN
    SELECT user_id INTO v_user FROM enrolled_semesters WHERE id = NEW.enrolled_semesters_id;

    SELECT string_agg(s.code, ', ' ORDER BY s.code) INTO v_missing
    FROM dependency_subject d
    JOIN subjects s ON s.id = d.dependency_id
    WHERE d.subject_id = NEW.subjects_id
      AND NOT EXISTS (
          SELECT 1
          FROM passed_subjects ps
          JOIN semesters_subjects ss ON ss.id = ps.enrolled_id
          JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
          WHERE es.user_id = v_user AND ss.subjects_id = d.dependency_id);

    IF v_missing IS NOT NULL THEN
        RAISE EXCEPTION 'Prerequisite(s) not passed for subject %: %',
            NEW.subjects_id, v_missing;
    END IF;
    RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS trg_prerequisites_passed ON semesters_subjects;
CREATE TRIGGER trg_prerequisites_passed
    BEFORE INSERT ON semesters_subjects
    FOR EACH ROW EXECUTE FUNCTION check_prerequisites_passed();

-- 4d. At most 5 subjects per enrolment, and at most 30 credits.
--     Deferred to commit, so a transaction may insert the five rows in any
--     order, or swap one subject for another, without tripping the rule
--     half-way through.
CREATE OR REPLACE FUNCTION check_enrolment_size()
    RETURNS TRIGGER
    LANGUAGE plpgsql
AS $$
DECLARE
    v_enrolment INTEGER := coalesce(NEW.enrolled_semesters_id, OLD.enrolled_semesters_id);
    v_count     INTEGER;
    v_credits   INTEGER;
BEGIN
    SELECT count(*), coalesce(sum(s.awarded_credits), 0)
      INTO v_count, v_credits
    FROM semesters_subjects ss
    JOIN subjects s ON s.id = ss.subjects_id
    WHERE ss.enrolled_semesters_id = v_enrolment;

    IF v_count > 5 THEN
        RAISE EXCEPTION 'Enrolment % has % subjects; the maximum is 5', v_enrolment, v_count;
    END IF;

    IF v_credits > 30 THEN
        RAISE EXCEPTION 'Enrolment % has % credits; the maximum is 30', v_enrolment, v_credits;
    END IF;

    RETURN NULL;
END;
$$;

DROP TRIGGER IF EXISTS trg_enrolment_size ON semesters_subjects;
CREATE CONSTRAINT TRIGGER trg_enrolment_size
    AFTER INSERT OR UPDATE OR DELETE ON semesters_subjects
    DEFERRABLE INITIALLY DEFERRED
    FOR EACH ROW EXECUTE FUNCTION check_enrolment_size();

-- ============================================================
--  5. GRADING RULES
-- ============================================================

CREATE OR REPLACE FUNCTION check_grade_rules()
    RETURNS TRIGGER
    LANGUAGE plpgsql
AS $$
DECLARE
    v_professor INTEGER;
    v_actor     INTEGER;
BEGIN
    IF NEW.date_passed > now() THEN
        RAISE EXCEPTION 'A grade cannot be dated in the future (%)', NEW.date_passed;
    END IF;

    SELECT professor_id INTO v_professor
    FROM semesters_subjects WHERE id = NEW.enrolled_id;

    -- Enforced only when the application tells the database who is acting,
    -- so scripts and migrations are not blocked.
    v_actor := nullif(current_setting('app.current_user_id', TRUE), '')::INTEGER;
    IF v_actor IS NOT NULL AND v_actor <> v_professor THEN
        RAISE EXCEPTION 'User % may not grade this subject; it is taught by %',
            v_actor, v_professor;
    END IF;

    -- Passing a subject implies the professor signed it off.
    UPDATE semesters_subjects SET signature = TRUE WHERE id = NEW.enrolled_id;

    RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS trg_grade_rules ON passed_subjects;
CREATE TRIGGER trg_grade_rules
    BEFORE INSERT OR UPDATE ON passed_subjects
    FOR EACH ROW EXECUTE FUNCTION check_grade_rules();

-- Tell the student, in their own language of record, that they were graded.
CREATE OR REPLACE FUNCTION notify_student_graded()
    RETURNS TRIGGER
    LANGUAGE plpgsql
AS $$
DECLARE
    v_user    INTEGER;
    v_subject TEXT;
BEGIN
    SELECT es.user_id, s.name
      INTO v_user, v_subject
    FROM semesters_subjects ss
    JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
    JOIN subjects s            ON s.id  = ss.subjects_id
    WHERE ss.id = NEW.enrolled_id;

    INSERT INTO notification (user_id, kind, message)
    VALUES (v_user, 'grade',
            format('Добивте оценка %s по предметот %s.', NEW.grade, v_subject));
    RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS trg_notify_graded ON passed_subjects;
CREATE TRIGGER trg_notify_graded
    AFTER INSERT ON passed_subjects
    FOR EACH ROW EXECUTE FUNCTION notify_student_graded();

-- ============================================================
--  6. VIEWS
-- ============================================================

CREATE OR REPLACE VIEW v_student_transcript AS
SELECT es.user_id,
       u."index"                    AS student_index,
       u.name || ' ' || u.surname   AS student,
       s.code                       AS subject_code,
       s.name                       AS subject,
       s.awarded_credits,
       ps.grade,
       ps.date_passed,
       p.name || ' ' || p.surname   AS professor,
       a.year,
       a.type                       AS semester_type
FROM passed_subjects ps
JOIN semesters_subjects ss ON ss.id = ps.enrolled_id
JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
JOIN users u               ON u.id  = es.user_id
JOIN subjects s            ON s.id  = ss.subjects_id
JOIN users p               ON p.id  = ss.professor_id
JOIN active_semesters a    ON a.id  = es.semester_id;

CREATE OR REPLACE VIEW v_student_standing AS
SELECT u.id                                          AS user_id,
       u."index"                                     AS student_index,
       u.name || ' ' || u.surname                    AS student,
       count(ps.id)                                  AS passed_subjects,
       coalesce(sum(s.awarded_credits), 0)           AS credits,
       round(avg(ps.grade::TEXT::INT)::NUMERIC, 2)   AS average_grade
FROM users u
LEFT JOIN enrolled_semesters es ON es.user_id = u.id
LEFT JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
LEFT JOIN passed_subjects ps    ON ps.enrolled_id = ss.id
LEFT JOIN subjects s            ON s.id = ss.subjects_id AND ps.id IS NOT NULL
WHERE u.role = 'student'
GROUP BY u.id, u."index", u.name, u.surname;

CREATE OR REPLACE VIEW v_professor_gradebook AS
SELECT ss.professor_id,
       p.name || ' ' || p.surname   AS professor,
       s.code                       AS subject_code,
       s.name                       AS subject,
       a.year,
       a.type                       AS semester_type,
       u.id                         AS student_id,
       u."index"                    AS student_index,
       u.name || ' ' || u.surname   AS student,
       ss.signature,
       ps.grade
FROM semesters_subjects ss
JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
JOIN users u               ON u.id  = es.user_id
JOIN users p               ON p.id  = ss.professor_id
JOIN subjects s            ON s.id  = ss.subjects_id
JOIN active_semesters a    ON a.id  = es.semester_id
LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id;

-- Which subjects nobody teaches in a given semester. Students cannot enrol
-- in these, so this is the list the administrator has to clear.
CREATE OR REPLACE VIEW v_semester_coverage AS
SELECT a.id      AS semester_id,
       a.year,
       a.type    AS semester_type,
       s.id      AS subject_id,
       s.code    AS subject_code,
       s.name    AS subject
FROM active_semesters a
CROSS JOIN subjects s
WHERE NOT EXISTS (SELECT 1 FROM professour_subjects ps
                   WHERE ps.active_semester_id = a.id AND ps.subject_id = s.id);

-- ============================================================
--  7. MATERIALIZED VIEW: subject statistics
-- ============================================================

DROP MATERIALIZED VIEW IF EXISTS mv_subject_statistics;
CREATE MATERIALIZED VIEW mv_subject_statistics AS
SELECT s.id                                                   AS subject_id,
       s.code                                                 AS subject_code,
       s.name                                                 AS subject,
       count(ss.id)                                           AS enrolled_count,
       count(ps.id)                                           AS passed_count,
       round(100.0 * count(ps.id) / nullif(count(ss.id), 0), 1) AS pass_rate,
       round(avg(ps.grade::TEXT::INT)::NUMERIC, 2)            AS average_grade
FROM subjects s
LEFT JOIN semesters_subjects ss ON ss.subjects_id = s.id
LEFT JOIN passed_subjects ps    ON ps.enrolled_id = ss.id
GROUP BY s.id, s.code, s.name;

CREATE UNIQUE INDEX mv_subject_statistics_pk ON mv_subject_statistics (subject_id);

-- ============================================================
--  8. BACKGROUND JOBS
-- ============================================================

-- Marks an enrolment finished once every subject in it is graded.
CREATE OR REPLACE PROCEDURE close_completed_enrolments()
    LANGUAGE plpgsql
AS $$
DECLARE
    v_closed INTEGER;
BEGIN
    WITH finished AS (
        SELECT es.id
        FROM enrolled_semesters es
        JOIN semesters_subjects ss ON ss.enrolled_semesters_id = es.id
        LEFT JOIN passed_subjects ps ON ps.enrolled_id = ss.id
        WHERE es.completed IS NULL
        GROUP BY es.id
        HAVING count(ss.id) > 0 AND count(ss.id) = count(ps.id)
    )
    UPDATE enrolled_semesters es
    SET completed   = now(),
        last_change = now()
    FROM finished f
    WHERE es.id = f.id;

    GET DIAGNOSTICS v_closed = ROW_COUNT;
    RAISE NOTICE 'close_completed_enrolments: closed % enrolment(s)', v_closed;
END;
$$;

-- Invalidates refresh tokens past their expiry.
CREATE OR REPLACE PROCEDURE expire_old_tokens()
    LANGUAGE plpgsql
AS $$
DECLARE
    v_expired INTEGER;
BEGIN
    UPDATE token
    SET is_valid = FALSE
    WHERE is_valid = TRUE AND expires_at < now();

    GET DIAGNOSTICS v_expired = ROW_COUNT;
    RAISE NOTICE 'expire_old_tokens: invalidated % token(s)', v_expired;
END;
$$;

-- Reminds students about subjects they are enrolled in without a signature.
CREATE OR REPLACE PROCEDURE notify_missing_signatures()
    LANGUAGE plpgsql
AS $$
DECLARE
    v_sent INTEGER;
BEGIN
    INSERT INTO notification (user_id, kind, message)
    SELECT es.user_id,
           'signature',
           format('Немате потпис по предметот %s за семестарот %s/%s.',
                  s.name, a.year, a.year + 1)
    FROM semesters_subjects ss
    JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
    JOIN subjects s            ON s.id  = ss.subjects_id
    JOIN active_semesters a    ON a.id  = es.semester_id
    WHERE ss.signature = FALSE
      AND es.completed IS NULL
      AND NOT EXISTS (
          SELECT 1 FROM notification n
          WHERE n.user_id = es.user_id
            AND n.kind = 'signature'
            AND n.message LIKE '%' || s.name || '%');

    GET DIAGNOSTICS v_sent = ROW_COUNT;
    RAISE NOTICE 'notify_missing_signatures: created % notification(s)', v_sent;
END;
$$;

CREATE OR REPLACE PROCEDURE refresh_statistics()
    LANGUAGE plpgsql
AS $$
BEGIN
    REFRESH MATERIALIZED VIEW CONCURRENTLY mv_subject_statistics;
    RAISE NOTICE 'refresh_statistics: mv_subject_statistics refreshed';
END;
$$;

-- One entry point for the scheduler (pg_cron, or the operating system).
CREATE OR REPLACE PROCEDURE run_nightly_maintenance()
    LANGUAGE plpgsql
AS $$
BEGIN
    CALL expire_old_tokens();
    CALL close_completed_enrolments();
    CALL notify_missing_signatures();
    CALL refresh_statistics();
    RAISE NOTICE 'run_nightly_maintenance: done';
END;
$$;

-- ============================================================
--  9. REPORTING FUNCTIONS
-- ============================================================

-- Everything the enrolment screen has to know about one student, in one call.
CREATE OR REPLACE FUNCTION student_eligible_subjects(p_user_id INTEGER, p_semester_id INTEGER)
    RETURNS TABLE (
        subject_id        INTEGER,
        subject_code      VARCHAR,
        subject_name      VARCHAR,
        awarded_credits   INTEGER,
        already_passed    BOOLEAN,
        prerequisites_ok  BOOLEAN,
        has_professor     BOOLEAN
    )
    LANGUAGE sql STABLE
AS $$
    SELECT s.id,
           s.code,
           s.name,
           s.awarded_credits::INTEGER,
           EXISTS (SELECT 1
                     FROM passed_subjects ps
                     JOIN semesters_subjects ss2 ON ss2.id = ps.enrolled_id
                     JOIN enrolled_semesters es2 ON es2.id = ss2.enrolled_semesters_id
                    WHERE es2.user_id = p_user_id AND ss2.subjects_id = s.id),
           NOT EXISTS (SELECT 1
                         FROM dependency_subject d
                        WHERE d.subject_id = s.id
                          AND NOT EXISTS (
                              SELECT 1
                                FROM passed_subjects ps
                                JOIN semesters_subjects ss3 ON ss3.id = ps.enrolled_id
                                JOIN enrolled_semesters es3 ON es3.id = ss3.enrolled_semesters_id
                               WHERE es3.user_id = p_user_id
                                 AND ss3.subjects_id = d.dependency_id)),
           EXISTS (SELECT 1 FROM professour_subjects pr
                    WHERE pr.subject_id = s.id AND pr.active_semester_id = p_semester_id)
    FROM subjects s
    WHERE EXISTS (
        SELECT 1
          FROM major_subjects ms
          JOIN enrolled_semesters es ON es.major_id = ms.major_id
         WHERE ms.subject_id = s.id AND es.user_id = p_user_id)
    ORDER BY s.code;
$$;

-- Average grade and credits of a student, as one row.
CREATE OR REPLACE FUNCTION student_summary(p_user_id INTEGER)
    RETURNS TABLE (passed_subjects BIGINT, credits BIGINT, average_grade NUMERIC)
    LANGUAGE sql STABLE
AS $$
    SELECT count(ps.id),
           coalesce(sum(s.awarded_credits), 0)::BIGINT,
           round(avg(ps.grade::TEXT::INT)::NUMERIC, 2)
    FROM passed_subjects ps
    JOIN semesters_subjects ss ON ss.id = ps.enrolled_id
    JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id
    JOIN subjects s            ON s.id  = ss.subjects_id
    WHERE es.user_id = p_user_id;
$$;
