-- Автопрокат: PostgreSQL 16, схема, роли, права и аудит операций

CREATE TABLE IF NOT EXISTS brand (
  brand_id  SERIAL PRIMARY KEY,
  name      VARCHAR(50) NOT NULL CONSTRAINT uq_brand_name UNIQUE,
  country   VARCHAR(50)
);

CREATE TABLE IF NOT EXISTS car (
  car_id        SERIAL PRIMARY KEY,
  license_plate VARCHAR(15) NOT NULL CONSTRAINT uq_car_plate UNIQUE,
  brand_id      INT NOT NULL CONSTRAINT fk_car_brand
                REFERENCES brand(brand_id) ON UPDATE CASCADE ON DELETE RESTRICT,
  year          SMALLINT CONSTRAINT chk_car_year CHECK (year BETWEEN 1990 AND 2030),
  daily_rate    NUMERIC(10,2) NOT NULL CONSTRAINT chk_car_rate CHECK (daily_rate > 0),
  status        VARCHAR(20) DEFAULT 'Свободен'
                CONSTRAINT chk_car_status CHECK (status IN ('Свободен','В аренде','На ТО'))
);

CREATE TABLE IF NOT EXISTS client (
  client_id  SERIAL PRIMARY KEY,
  full_name  VARCHAR(100) NOT NULL,
  phone      VARCHAR(20) CONSTRAINT uq_client_phone UNIQUE,
  address    TEXT,
  license_no VARCHAR(20) CONSTRAINT uq_client_license UNIQUE,
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE IF NOT EXISTS rental (
  rental_id     SERIAL PRIMARY KEY,
  car_id        INT NOT NULL CONSTRAINT fk_rental_car
                REFERENCES car(car_id) ON UPDATE CASCADE ON DELETE RESTRICT,
  client_id     INT NOT NULL CONSTRAINT fk_rental_client
                REFERENCES client(client_id) ON UPDATE CASCADE ON DELETE RESTRICT,
  start_date    DATE NOT NULL,
  end_date      DATE NOT NULL CONSTRAINT chk_rental_dates CHECK (end_date >= start_date),
  actual_return DATE,
  total_cost    NUMERIC(10,2) CONSTRAINT chk_rental_cost CHECK (total_cost >= 0),
  status        VARCHAR(20) DEFAULT 'Активна'
                CONSTRAINT chk_rental_status CHECK (status IN ('Активна','Завершена'))
);

CREATE INDEX IF NOT EXISTS idx_car_brand_id ON car(brand_id);
CREATE INDEX IF NOT EXISTS idx_rental_car_id ON rental(car_id);
CREATE INDEX IF NOT EXISTS idx_rental_client_id ON rental(client_id);
CREATE INDEX IF NOT EXISTS idx_rental_dates ON rental(start_date, end_date);
CREATE INDEX IF NOT EXISTS idx_rental_status ON rental(status);

CREATE OR REPLACE VIEW v_rentals_full AS
  SELECT r.rental_id,
         c.full_name AS client_name,
         c.phone AS client_phone,
         ca.license_plate,
         b.name AS brand_name,
         r.start_date,
         r.end_date,
         r.actual_return,
         r.total_cost,
         r.status
  FROM rental r
  JOIN client c ON c.client_id = r.client_id
  JOIN car ca ON ca.car_id = r.car_id
  JOIN brand b ON b.brand_id = ca.brand_id;

CREATE OR REPLACE VIEW v_available_cars AS
  SELECT ca.car_id, ca.license_plate, b.name AS brand, b.country, ca.year, ca.daily_rate
  FROM car ca
  JOIN brand b ON b.brand_id = ca.brand_id
  WHERE ca.status = 'Свободен';

CREATE OR REPLACE VIEW v_client_stats AS
  SELECT c.client_id,
         c.full_name,
         COUNT(r.rental_id) AS total_rentals,
         COALESCE(SUM(r.total_cost), 0) AS total_spent
  FROM client c
  LEFT JOIN rental r ON r.client_id = c.client_id
  GROUP BY c.client_id, c.full_name;

-- Пользователи предметной области
CREATE ROLE rent_admin NOLOGIN;
CREATE ROLE rent_manager NOLOGIN;
CREATE ROLE rent_viewer NOLOGIN;

-- Администратор: полный доступ
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO rent_admin;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO rent_admin;

-- Менеджер: рабочий CRUD без управления пользователями и системной структурой
GRANT SELECT, INSERT, UPDATE ON client, car, rental TO rent_manager;
GRANT SELECT ON brand, v_rentals_full, v_available_cars, v_client_stats TO rent_manager;
GRANT USAGE ON SEQUENCE client_client_id_seq, car_car_id_seq, rental_rental_id_seq TO rent_manager;

-- Аналитик: только чтение и отчеты
GRANT SELECT ON brand, car, client, rental, v_rentals_full, v_available_cars, v_client_stats TO rent_viewer;

-- Примеры учетных записей БД. В реальном проекте пароли задаются администратором отдельно.
-- CREATE USER ivanov WITH PASSWORD 'Str0ng#Pass1' IN ROLE rent_manager;
-- CREATE USER petrov WITH PASSWORD 'Str0ng#Pass2' IN ROLE rent_viewer;
-- CREATE USER db_admin WITH PASSWORD 'Str0ng#Admin1' IN ROLE rent_admin;

-- Row Level Security: явное разрешение по ролям.
ALTER TABLE rental ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS rental_admin_all ON rental;
CREATE POLICY rental_admin_all ON rental
  FOR ALL TO rent_admin
  USING (true)
  WITH CHECK (true);

DROP POLICY IF EXISTS rental_manager_all ON rental;
CREATE POLICY rental_manager_all ON rental
  FOR ALL TO rent_manager
  USING (true)
  WITH CHECK (true);

DROP POLICY IF EXISTS rental_viewer_read ON rental;
CREATE POLICY rental_viewer_read ON rental
  FOR SELECT TO rent_viewer
  USING (true);

-- Аудит операций средствами СУБД
CREATE TABLE IF NOT EXISTS audit_log (
  audit_id   BIGSERIAL PRIMARY KEY,
  changed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  db_user    TEXT NOT NULL DEFAULT current_user,
  action     TEXT NOT NULL,
  table_name TEXT NOT NULL,
  row_id     TEXT,
  old_data   JSONB,
  new_data   JSONB
);

CREATE OR REPLACE FUNCTION fn_audit_row()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
  pk_name TEXT := TG_ARGV[0];
BEGIN
  IF TG_OP = 'INSERT' THEN
    INSERT INTO audit_log(action, table_name, row_id, new_data)
    VALUES (TG_OP, TG_TABLE_NAME, to_jsonb(NEW)->>pk_name, to_jsonb(NEW));
    RETURN NEW;
  ELSIF TG_OP = 'UPDATE' THEN
    INSERT INTO audit_log(action, table_name, row_id, old_data, new_data)
    VALUES (TG_OP, TG_TABLE_NAME, to_jsonb(NEW)->>pk_name, to_jsonb(OLD), to_jsonb(NEW));
    RETURN NEW;
  ELSE
    INSERT INTO audit_log(action, table_name, row_id, old_data)
    VALUES (TG_OP, TG_TABLE_NAME, to_jsonb(OLD)->>pk_name, to_jsonb(OLD));
    RETURN OLD;
  END IF;
END;
$$;

DROP TRIGGER IF EXISTS trg_audit_brand ON brand;
CREATE TRIGGER trg_audit_brand
AFTER INSERT OR UPDATE OR DELETE ON brand
FOR EACH ROW EXECUTE FUNCTION fn_audit_row('brand_id');

DROP TRIGGER IF EXISTS trg_audit_car ON car;
CREATE TRIGGER trg_audit_car
AFTER INSERT OR UPDATE OR DELETE ON car
FOR EACH ROW EXECUTE FUNCTION fn_audit_row('car_id');

DROP TRIGGER IF EXISTS trg_audit_client ON client;
CREATE TRIGGER trg_audit_client
AFTER INSERT OR UPDATE OR DELETE ON client
FOR EACH ROW EXECUTE FUNCTION fn_audit_row('client_id');

DROP TRIGGER IF EXISTS trg_audit_rental ON rental;
CREATE TRIGGER trg_audit_rental
AFTER INSERT OR UPDATE OR DELETE ON rental
FOR EACH ROW EXECUTE FUNCTION fn_audit_row('rental_id');
