CREATE TYPE usertitle_status_t AS ENUM ('finished', 'planned', 'dropped', 'in-progress');
CREATE TYPE storage_type_t AS ENUM ('local', 's3');
CREATE TYPE title_status_t AS ENUM ('finished', 'ongoing', 'planned');
CREATE TYPE release_season_t AS ENUM ('winter', 'spring', 'summer', 'fall');

CREATE TABLE providers (
    id                  bigint              GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    provider_name       text                NOT NULL,
    credentials         jsonb
);

CREATE TABLE tags (
    id                  bigint              GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    -- example: { "ru": "Сёдзё", "en": "Shojo", "jp": "少女"}
    tag_names           jsonb               NOT NULL
);

CREATE TABLE images (
    id                  bigint              GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    storage_type        storage_type_t      NOT NULL,
    image_path          text                UNIQUE NOT NULL
);

CREATE TABLE users (
    id                  bigint              GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    avatar_id           bigint              REFERENCES images (id) ON DELETE SET NULL,
    passhash            text                NOT NULL,
    mail                text                CHECK (mail ~ '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+$'),
    nickname            text                UNIQUE NOT NULL CHECK (nickname ~ '^[a-zA-Z0-9_-]{3,}$'),
    disp_name           text,
    user_desc           text,
    creation_date       timestamptz         NOT NULL DEFAULT NOW(),
    last_login          timestamptz
);

CREATE TABLE studios (
    id                  bigint              GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    studio_name         text                NOT NULL UNIQUE,
    illust_id           bigint              REFERENCES images (id) ON DELETE SET NULL,
    studio_desc         text
);

CREATE TABLE titles (
    -- // TODO: anime type (film, season etc)
    id                  bigint              GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    -- example {"ru": ["Атака титанов", "Атака Титанов"],"en": ["Attack on Titan", "AoT"],"ja": ["進撃の巨人", "しんげきのきょじん"]}
    title_names         jsonb               NOT NULL,
    studio_id           bigint              NOT NULL REFERENCES studios (id),
    poster_id           bigint              REFERENCES images (id) ON DELETE SET NULL,
    title_status        title_status_t      NOT NULL,                          
    rating              float               CHECK (rating >= 0 AND rating <= 10),
    rating_count        int                 CHECK (rating_count >= 0),
    release_year        int                 CHECK (release_year >= 1900),
    release_season      release_season_t,
    season              int                 CHECK (season >= 0),
    episodes_aired      int                 CHECK (episodes_aired >= 0),
    episodes_all        int                 CHECK (episodes_all >= 0),
    -- example { "1": "50.50", "2": "23.23"}
    episodes_len        jsonb,
    CHECK ((episodes_aired IS NULL AND episodes_all IS NULL)
        OR (episodes_aired IS NOT NULL AND episodes_all IS NOT NULL 
        AND episodes_aired <= episodes_all))
);

CREATE TABLE reviews (
    id                  bigint              GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    data                text                NOT NULL,
    rating              int                 CHECK (rating >= 0 AND rating <= 10),
    user_id             bigint              REFERENCES users (id) ON DELETE SET NULL,
    title_id            bigint              REFERENCES titles (id) ON DELETE CASCADE,
    created_at          timestamptz         DEFAULT NOW()
);

CREATE TABLE review_images (
                                            PRIMARY KEY (review_id, image_id),
    review_id           bigint              NOT NULL REFERENCES reviews(id) ON DELETE CASCADE,
    image_id            bigint              NOT NULL REFERENCES images(id) ON DELETE CASCADE
);

CREATE TABLE usertitles (
                                            PRIMARY KEY (user_id, title_id),
    user_id             bigint              NOT NULL REFERENCES users (id) ON DELETE CASCADE,
    title_id            bigint              NOT NULL REFERENCES titles (id) ON DELETE CASCADE,
    status              usertitle_status_t  NOT NULL,
    rate                int                 CHECK (rate > 0 AND rate <= 10),
    review_id           bigint              REFERENCES reviews (id) ON DELETE SET NULL,
    ctime               timestamptz         NOT NULL DEFAULT now()
    -- // TODO: series status
);

CREATE TABLE title_tags (
                                            PRIMARY KEY (title_id, tag_id),
    title_id            bigint              NOT NULL REFERENCES titles (id) ON DELETE CASCADE,
    tag_id              bigint              NOT NULL REFERENCES tags (id) ON DELETE CASCADE
);

CREATE TABLE signals (
    id                  bigint              GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    title_id            bigint              REFERENCES titles (id),
    raw_data            jsonb               NOT NULL,
    provider_id         bigint              NOT NULL REFERENCES providers (id),
    pending             boolean             NOT NULL
);

CREATE TABLE external_services (
    id                  bigint              GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name                text                UNIQUE NOT NULL,
    auth_token          text
);

CREATE TABLE external_ids (
    user_id             bigint              NOT NULL REFERENCES users (id),
    service_id          bigint              REFERENCES external_services (id),
    external_id         text                NOT NULL
);

-- Functions
CREATE OR REPLACE FUNCTION update_title_rating()
RETURNS TRIGGER AS $$
BEGIN
    IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE' AND NEW.rate IS DISTINCT FROM OLD.rate) THEN
        UPDATE titles
        SET 
            rating = sub.avg_rating,
            rating_count = sub.rating_count
        FROM (
            SELECT 
                title_id,
                AVG(rate)::float AS avg_rating,
                COUNT(rate) AS rating_count
            FROM usertitles
            WHERE title_id = NEW.title_id AND rate IS NOT NULL
            GROUP BY title_id
        ) AS sub
        WHERE titles.id = sub.title_id;
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION notify_new_signal()
RETURNS TRIGGER AS $$
DECLARE
    payload JSON;
BEGIN
    payload := json_build_object(
        'signal_id', NEW.id,
        'title_id', NEW.title_id,
        'provider_id', NEW.provider_id,
        'pending', NEW.pending,
        'timestamp', NOW()
    );
    PERFORM pg_notify('new_signal', payload::text);

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Triggers

CREATE TRIGGER trg_update_title_rating
AFTER INSERT OR UPDATE OF rate ON usertitles
FOR EACH ROW
EXECUTE FUNCTION update_title_rating();

CREATE TRIGGER trg_notify_new_signal
AFTER INSERT ON signals
FOR EACH ROW
EXECUTE FUNCTION notify_new_signal();

CREATE OR REPLACE FUNCTION set_ctime()
RETURNS TRIGGER AS $$
BEGIN
    NEW.ctime = now();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_ctime_on_update
BEFORE UPDATE ON usertitles
FOR EACH ROW
EXECUTE FUNCTION set_ctime();