-- =======================================
-- NAIL SALON DATABASE SCHEMA
-- Complete init.sql with GDPR & Tax Compliance
-- =======================================

-- Enable pgcrypto for generating random IDs
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- =======================================
-- ENUMS
-- =======================================

CREATE TYPE account_role AS ENUM ('unverified_email', 'verified_email', 'admin', 'guest', 'affiliate');
CREATE TYPE account_type AS ENUM ('email', 'google', 'microsoft', 'facebook', 'guest');
CREATE TYPE payment_type AS ENUM ('deposit', 'full', 'tip', 'balance', 'partial');
CREATE TYPE payment_method AS ENUM ('online_square', 'in_person_card', 'cash', 'giftcard', 'discount', 'on_the_house');
CREATE TYPE payment_status AS ENUM ('pending', 'completed', 'failed', 'refunded');
CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'in_progress', 'completed', 'client_cancelled', 'we_cancelled', 'no_show', 'pending_release', 'deposit_lapsed');
CREATE TYPE campaign_type AS ENUM ('time_based', 'milestone');
CREATE TYPE milestone_type AS ENUM ('per_user_booking_count', 'global_booking_count', 'anniversary');
CREATE TYPE milestone_unit AS ENUM ('bookings', 'months', 'years');
CREATE TYPE discount_campaign_scope AS ENUM ('all_bookings', 'first_booking_only', 'new_customers_only');
CREATE TYPE discount_campaign_status AS ENUM ('draft', 'active', 'completed', 'cancelled');
CREATE TYPE till_item_type AS ENUM ('gift_card', 'retail_product');

-- =======================================
-- SHORT ID GENERATION
-- =======================================

CREATE OR REPLACE FUNCTION generate_short_id(table_name TEXT)
RETURNS CHAR(12) AS $$
DECLARE
    new_id CHAR(12);
    collision_count INT := 0;
    max_attempts INT := 100;
BEGIN
    LOOP
        new_id := substr(encode(gen_random_bytes(6), 'hex'), 1, 12);

        EXECUTE format('SELECT 1 FROM %I WHERE id = $1 LIMIT 1', table_name) USING new_id;

        IF NOT FOUND THEN
            RETURN new_id;
        END IF;

        collision_count := collision_count + 1;
        IF collision_count >= max_attempts THEN
            RAISE EXCEPTION 'Unable to generate unique ID after % attempts for table %', max_attempts, table_name;
        END IF;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION generate_user_id() RETURNS CHAR(12) AS $$ SELECT generate_short_id('users'); $$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_service_id() RETURNS CHAR(12) AS $$ SELECT generate_short_id('services'); $$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_booking_id() RETURNS CHAR(12) AS $$ SELECT generate_short_id('bookings'); $$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_payment_id() RETURNS CHAR(12) AS $$ SELECT generate_short_id('payments'); $$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_user_saved_card_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('user_saved_cards');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_refund_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('refunds');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_till_sale_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('till_sales');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_affiliate_payout_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('affiliate_payouts');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_square_deposit_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('square_deposits');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_patch_test_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('patch_tests');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_user_patch_test_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('user_patch_tests');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_user_referrals_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('user_referrals');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_referral_discounts_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('referral_discounts');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_user_social_logins_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('user_social_logins');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_name_history_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('name_history');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_custom_services_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('custom_services');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_verification_codes_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('verification_codes');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_time_blockers_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('time_blockers');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_forgiven_no_shows_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('forgiven_no_shows');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_loyalty_redemptions_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('loyalty_redemptions');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_discount_campaigns_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('discount_campaigns');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_booking_discounts_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('booking_discounts');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_tags_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('tags');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_admin_notifications_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('admin_notifications');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_user_notification_preferences_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('user_notification_preferences');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_images_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('images');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_gift_cards_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('gift_cards');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_gift_card_transactions_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('gift_card_transactions');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_gift_card_expired_balances_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('gift_card_expired_balances');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_admin_audit_log_id() RETURNS CHAR(12) AS $$
    SELECT generate_short_id('admin_audit_log');
$$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_verification_code() RETURNS CHAR(12) AS $$ SELECT substr(encode(gen_random_bytes(6), 'hex'), 1, 12); $$ LANGUAGE sql;

CREATE OR REPLACE FUNCTION generate_referral_code()
RETURNS CHAR(12) AS $$
DECLARE
    new_code CHAR(12);
    collision_count INT := 0;
    max_attempts INT := 100;
BEGIN
    LOOP
        new_code := substr(encode(gen_random_bytes(6), 'hex'), 1, 12);

        -- Check against referral_code column in users table (not id column)
        PERFORM 1 FROM users WHERE referral_code = new_code LIMIT 1;

        IF NOT FOUND THEN
            RETURN new_code;
        END IF;

        collision_count := collision_count + 1;
        IF collision_count >= max_attempts THEN
            RAISE EXCEPTION 'Unable to generate unique referral code after % attempts', max_attempts;
        END IF;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- =======================================
-- USERS TABLE
-- =======================================

CREATE TABLE users (
    id CHAR(12) PRIMARY KEY DEFAULT generate_user_id(),
    -- Identity fields
    n_first_name VARCHAR(50) NOT NULL,          -- vCard N.given
    n_last_name VARCHAR(50) NOT NULL,           -- vCard N.family
    fn VARCHAR(120) GENERATED ALWAYS AS (n_first_name || ' ' || n_last_name) STORED, -- vCard FN
    email VARCHAR(255),                 -- vCard EMAIL (nullable for social-only/guest)
                                       -- Uniqueness enforced only for non-guest users via partial index
    phone VARCHAR(20) NOT NULL,                         -- vCard TEL
    date_of_birth DATE NOT NULL,                        -- vCard BDAY
    profile_pic_url TEXT,                      -- vCard PHOTO
    -- Account fields
    account_role account_role NOT NULL DEFAULT 'unverified_email', -- initial signup role
    account_type account_type NOT NULL DEFAULT 'email', -- initial signup type (info only)
    -- Security fields
    password_hash TEXT,                         -- NULL for pure social logins
    last_login_at TIMESTAMPTZ DEFAULT NOW(),
    -- Account lockout fields
    failed_attempts INT NOT NULL DEFAULT 0,
    locked_until TIMESTAMPTZ,
    -- Loyalty fields
    loyalty_stamps INT NOT NULL DEFAULT 0,
    referral_code CHAR(12) UNIQUE DEFAULT generate_referral_code(),
    -- GDPR fields
    privacy_policy_and_terms_consent BOOLEAN NOT NULL DEFAULT TRUE,
    policy_consent_updated_at TIMESTAMPTZ DEFAULT NOW(),
    data_retention_consent BOOLEAN NOT NULL DEFAULT TRUE,
    data_consent_updated_at TIMESTAMPTZ DEFAULT NOW(),
    -- Audit fields
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    -- Deposit tracking: remaining deposits needed (0-3). Reduces by 1 when booking with payment completes.
    deposits_required INT NOT NULL DEFAULT 0,
    -- staff fields
    notes TEXT
);

CREATE TABLE user_social_logins (
    id CHAR(12) PRIMARY KEY DEFAULT generate_user_social_logins_id(),
    user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    provider account_type NOT NULL CHECK (provider IN ('google', 'microsoft', 'facebook')),
    immutable_id TEXT NOT NULL,                  -- the stable provider user ID
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    UNIQUE (provider, immutable_id),             -- prevents duplicate identities
    UNIQUE (user_id, provider)                   -- one account per provider per user
);


CREATE INDEX idx_users_email_lower ON users (LOWER(email));
CREATE INDEX idx_users_account_role ON users (account_role);
-- pg_trgm GIN indexes for ILIKE search on user-facing columns
CREATE INDEX idx_users_fn_trgm ON users USING GIN (fn gin_trgm_ops);
CREATE INDEX idx_users_email_trgm ON users USING GIN (email gin_trgm_ops);
CREATE INDEX idx_users_phone_trgm ON users USING GIN (phone gin_trgm_ops);
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX IF NOT EXISTS idx_users_n_first_name_trgm ON users USING GIN (n_first_name gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_users_n_last_name_trgm ON users USING GIN (n_last_name gin_trgm_ops);

-- Enforce unique email only for registered (non-guest) users
-- Guest accounts can share emails; registered accounts cannot
CREATE UNIQUE INDEX idx_users_email_registered ON users (email) WHERE account_role != 'guest';

-- =======================================
-- NAME HISTORY TABLE
-- =======================================
-- WHY: Tracks user-initiated first/last name changes for display in booking modals
--      and GDPR export. History is used to show "(formerly [old first] [old last])"
--      on completed booking receipts and admin views.

CREATE TABLE name_history (
    id CHAR(12) PRIMARY KEY DEFAULT generate_name_history_id(),
    user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    previous_first_name VARCHAR(50) NOT NULL,
    previous_last_name VARCHAR(50) NOT NULL,
    booking_id CHAR(12),
    changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_name_history_user_id ON name_history(user_id);
CREATE INDEX idx_name_history_changed_at ON name_history(changed_at);
CREATE INDEX idx_name_history_user_active ON name_history(user_id) WHERE booking_id IS NULL;

-- =======================================
-- VERIFICATION CODES TABLE
-- =======================================
CREATE TYPE verification_purpose AS ENUM ('email_verify', 'password_reset');

CREATE TABLE verification_codes (
    id CHAR(12) PRIMARY KEY DEFAULT generate_verification_codes_id(),
    code CHAR(12) NOT NULL UNIQUE DEFAULT generate_verification_code(),
    user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    purpose verification_purpose NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL,
    used_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_verification_codes_code ON verification_codes (code);
CREATE INDEX idx_verification_codes_user_purpose ON verification_codes (user_id, purpose) WHERE used_at IS NULL;
CREATE INDEX idx_verification_codes_expires ON verification_codes (expires_at) WHERE used_at IS NULL;

-- =======================================
-- PATCH TESTS TABLE
-- =======================================

CREATE TABLE patch_tests (
    id CHAR(12) PRIMARY KEY DEFAULT generate_patch_test_id(),
    name VARCHAR(100) NOT NULL,
    description TEXT,
    notice_duration_hours INT NOT NULL DEFAULT 24,
    expiry_months INT NOT NULL DEFAULT 6,
    service_ids CHAR(12)[] DEFAULT '{}'
);

CREATE INDEX idx_patch_tests_name ON patch_tests(name);

-- =======================================
-- USER PATCH TESTS TABLE
-- =======================================

CREATE TABLE user_patch_tests (
    id CHAR(12) PRIMARY KEY DEFAULT generate_user_patch_test_id(),
    user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    patch_test_id CHAR(12) NOT NULL REFERENCES patch_tests(id) ON DELETE CASCADE,
    tested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (user_id, patch_test_id)
);

CREATE INDEX idx_user_patch_tests_user ON user_patch_tests(user_id);
CREATE INDEX idx_user_patch_tests_tested_at ON user_patch_tests(tested_at);

-- =======================================
-- SERVICES TABLE
-- =======================================

CREATE TABLE services (
    id CHAR(12) PRIMARY KEY DEFAULT generate_service_id(),
    name VARCHAR(100) NOT NULL,
    description TEXT,
    price NUMERIC(10,2) NOT NULL,
    duration_minutes INT NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    minimum_age_required INT NOT NULL DEFAULT 16,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by CHAR(12)
);

CREATE INDEX idx_services_name ON services(name);
CREATE INDEX IF NOT EXISTS idx_services_name_trgm ON services USING GIN (name gin_trgm_ops);

-- =======================================
-- CUSTOM SERVICES TABLE (One-off / special request services)
-- =======================================

CREATE TABLE custom_services (
    id CHAR(12) PRIMARY KEY DEFAULT generate_custom_services_id(),
    name VARCHAR(100) NOT NULL,
    description TEXT,
    price NUMERIC(10,2) NOT NULL,
    duration_minutes INT NOT NULL,
    minimum_age_required INT NOT NULL DEFAULT 0,
    notes TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by CHAR(12) REFERENCES users(id),
    usage_count INT NOT NULL DEFAULT 0,
    last_used_at TIMESTAMPTZ
);

CREATE INDEX idx_custom_services_name ON custom_services(name);
CREATE INDEX idx_custom_services_name_trgm ON custom_services USING GIN (name gin_trgm_ops);
CREATE INDEX idx_custom_services_desc_trgm ON custom_services USING GIN (description gin_trgm_ops);
CREATE INDEX idx_custom_services_usage ON custom_services(usage_count DESC);

-- =======================================
-- BOOKINGS TABLE
-- =======================================

CREATE TABLE bookings (
    id CHAR(12) PRIMARY KEY DEFAULT generate_booking_id(),
    user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    start_time TIMESTAMPTZ NOT NULL,
    status booking_status NOT NULL DEFAULT 'pending',
    notes TEXT,
    deposit_required BOOLEAN NOT NULL DEFAULT FALSE,
    -- Computed fields (maintained by trigger on booking_services/booking_custom_services)
    total_duration_minutes INT NOT NULL DEFAULT 60,
    total_amount NUMERIC(10,2) NOT NULL DEFAULT 0,
    end_time TIMESTAMPTZ NOT NULL,  -- Set by BEFORE INSERT trigger; recalculated by booking_services trigger
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by CHAR(12),
    idempotency_key VARCHAR(64) UNIQUE,
    out_of_hours BOOLEAN NOT NULL DEFAULT FALSE
);

CREATE INDEX idx_bookings_userid ON bookings(user_id);
CREATE INDEX idx_bookings_userid_starttime ON bookings(user_id, start_time);
CREATE INDEX idx_bookings_end_time ON bookings(end_time);
CREATE INDEX idx_bookings_status_end_time ON bookings(status, end_time);
CREATE INDEX IF NOT EXISTS idx_bookings_notes_trgm ON bookings USING GIN (notes gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_bookings_start_time ON bookings(start_time);

ALTER TABLE name_history ADD CONSTRAINT fk_name_history_booking_id
    FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE SET NULL;

-- =======================================
-- BOOKING SERVICES JUNCTION TABLE
-- =======================================

CREATE TABLE booking_services (
    booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
    service_id CHAR(12) NOT NULL REFERENCES services(id) ON DELETE RESTRICT,
    override_price NUMERIC(10,2),
    override_duration_minutes INT,
    PRIMARY KEY (booking_id, service_id)
);

CREATE INDEX idx_booking_services_service_id ON booking_services(service_id);

-- =======================================
-- BOOKING CUSTOM SERVICES JUNCTION TABLE
-- =======================================

CREATE TABLE booking_custom_services (
    booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
    custom_service_id CHAR(12) NOT NULL REFERENCES custom_services(id) ON DELETE RESTRICT,
    override_price NUMERIC(10,2),
    override_duration_minutes INT,
    PRIMARY KEY (booking_id, custom_service_id)
);

CREATE INDEX idx_booking_custom_services_custom_service_id ON booking_custom_services(custom_service_id);

-- =======================================
-- TRIGGER: Auto-recalculate booking duration & total
-- =======================================
-- WHY: Keeps bookings.total_duration_minutes, bookings.total_amount,
--      and the generated bookings.end_time in sync when services change.

CREATE OR REPLACE FUNCTION recalc_booking_duration_and_total()
RETURNS TRIGGER AS $$
DECLARE
    bid CHAR(12);
BEGIN
    bid := COALESCE(NEW.booking_id, OLD.booking_id);
    UPDATE bookings b SET
        total_duration_minutes = sub.dur,
        total_amount = sub.amt,
        end_time = b.start_time + (sub.dur * INTERVAL '1 minute')
    FROM (
        SELECT
            COALESCE((SELECT SUM(dur) FROM (
                SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
                FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bid
                UNION ALL
                SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
                FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bid
            ) sub2), 60) AS dur,
            COALESCE((SELECT SUM(amt) FROM (
                SELECT COALESCE(bs.override_price, s.price) AS amt
                FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bid
                UNION ALL
                SELECT COALESCE(bcs.override_price, cs.price)
                FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bid
            ) sub2), 0) AS amt
    ) sub
    WHERE b.id = bid;
    RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_recalc_booking_duration_total
    AFTER INSERT OR UPDATE OR DELETE ON booking_services
    FOR EACH ROW EXECUTE FUNCTION recalc_booking_duration_and_total();

CREATE TRIGGER trg_recalc_booking_duration_total_custom
    AFTER INSERT OR UPDATE OR DELETE ON booking_custom_services
    FOR EACH ROW EXECUTE FUNCTION recalc_booking_duration_and_total();

-- BEFORE INSERT trigger on bookings to set end_time from defaults
-- (the AFTER trigger on booking_services overrides this when services are added)
CREATE OR REPLACE FUNCTION set_booking_end_time()
RETURNS TRIGGER AS $$
BEGIN
    NEW.end_time := NEW.start_time + (NEW.total_duration_minutes * INTERVAL '1 minute');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_set_booking_end_time
    BEFORE INSERT ON bookings
    FOR EACH ROW
    EXECUTE FUNCTION set_booking_end_time();

-- =======================================
-- BOOKING EDIT REQUESTS TABLE
-- =======================================

CREATE TABLE booking_edit_requests (
    id CHAR(12) PRIMARY KEY DEFAULT generate_booking_id(),
    booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
    requested_by CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    new_start_time TIMESTAMPTZ,
    new_services CHAR(12)[] DEFAULT '{}',
    notes TEXT,
    has_overrides BOOLEAN NOT NULL DEFAULT FALSE,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT chk_at_least_one_field CHECK (
        new_start_time IS NOT NULL OR
        array_length(new_services, 1) IS NOT NULL OR
        notes IS NOT NULL
    )
);

CREATE INDEX idx_booking_edit_requests_booking ON booking_edit_requests(booking_id);
CREATE INDEX idx_booking_edit_requests_requested_by ON booking_edit_requests(requested_by);

CREATE TABLE user_referrals (
    id CHAR(12) PRIMARY KEY DEFAULT generate_user_referrals_id(),
    referrer_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    referred_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    referred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    claimed_booking_id CHAR(12) REFERENCES bookings(id) ON DELETE SET NULL,
    UNIQUE (referrer_id, referred_id)
);

-- =======================================
-- REFERRAL DISCOUNTS TABLE
-- =======================================
CREATE TABLE referral_discounts (
    id CHAR(12) PRIMARY KEY DEFAULT generate_referral_discounts_id(),
    user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    referral_id CHAR(12) NOT NULL REFERENCES user_referrals(id) ON DELETE CASCADE,
    discount_percent NUMERIC(5,2) NOT NULL DEFAULT 10.00,
    used BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    used_at TIMESTAMPTZ
);
CREATE INDEX idx_referral_discounts_user_id ON referral_discounts(user_id);

-- =======================================
-- DEFAULT WORKING HOURS TABLE
-- =======================================
CREATE TABLE working_hours (
    weekday SMALLINT PRIMARY KEY,    -- 0 = Monday, 6 = Sunday
    start_time TIME NOT NULL,        -- e.g. 09:00
    end_time TIME NOT NULL,          -- e.g. 17:00
    is_open BOOLEAN NOT NULL DEFAULT TRUE
);

-- weekday is already indexed via PRIMARY KEY — no separate index needed
INSERT INTO working_hours VALUES (0, '00:00:00', '00:00:00', FALSE);
INSERT INTO working_hours VALUES (1, '09:00:00', '17:00:00', TRUE);
INSERT INTO working_hours VALUES (2, '09:00:00', '17:00:00', TRUE);
INSERT INTO working_hours VALUES (3, '12:00:00', '20:00:00', TRUE);
INSERT INTO working_hours VALUES (4, '09:00:00', '17:00:00', TRUE);
INSERT INTO working_hours VALUES (5, '09:00:00', '17:00:00', TRUE);
INSERT INTO working_hours VALUES (6, '00:00:00', '00:00:00', FALSE);

-- =======================================
-- EXCEPTIONAL WORKING HOURS
-- =======================================

-- Exceptional working hours groups (template)
CREATE TABLE exceptional_working_hours_groups (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,             -- e.g. "Christmas Schedule"
    description TEXT NOT NULL       -- e.g. "Extended hours for holiday period"
);

-- Exceptional working hours (7 entries per group, one per weekday)
CREATE TABLE exceptional_working_hours (
    id SERIAL PRIMARY KEY,
    group_id INT NOT NULL REFERENCES exceptional_working_hours_groups(id) ON DELETE CASCADE,
    weekday SMALLINT NOT NULL,      -- 0 = Monday ... 6 = Sunday
    start_time TIME NOT NULL,
    end_time TIME NOT NULL,
    is_open BOOLEAN NOT NULL DEFAULT TRUE
);

CREATE UNIQUE INDEX idx_group_weekday ON exceptional_working_hours(group_id, weekday);

-- Exceptional group applications (assign a group to a specific week)
CREATE TABLE exceptional_group_applications (
    id SERIAL PRIMARY KEY,
    group_id INT NOT NULL REFERENCES exceptional_working_hours_groups(id) ON DELETE CASCADE,
    week_start DATE NOT NULL        -- Monday of the week this group applies to
);

CREATE UNIQUE INDEX idx_group_application_week ON exceptional_group_applications(week_start);

-- =======================================
-- TIME BLOCKERS TABLE
-- =======================================
-- Admin-defined time periods that are unavailable for booking
-- Used for doctor appointments, extended lunch, training, etc.

CREATE TABLE time_blockers (
    id CHAR(12) PRIMARY KEY DEFAULT generate_time_blockers_id(),
    start_time TIMESTAMPTZ NOT NULL,
    duration_minutes INT NOT NULL CHECK (duration_minutes > 0),
    description TEXT,
    cron_expression TEXT,  -- NULL = one-off, otherwise cron pattern for recurring
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by CHAR(12) REFERENCES users(id) ON DELETE SET NULL
);

-- Indexes for efficient overlap queries
CREATE INDEX idx_time_blockers_start_time ON time_blockers(start_time);
CREATE INDEX idx_time_blockers_cron ON time_blockers(cron_expression) WHERE cron_expression IS NOT NULL;
CREATE INDEX idx_time_blockers_created_by ON time_blockers(created_by);
CREATE INDEX idx_time_blockers_created_at ON time_blockers(created_at);
CREATE INDEX IF NOT EXISTS idx_time_blockers_desc_trgm ON time_blockers USING GIN (description gin_trgm_ops);

-- =======================================
-- FORGIVEN NO-SHOWS TABLE
-- =======================================
-- Tracks which no-shows have been forgiven (by full deposit payment)
CREATE TABLE forgiven_no_shows (
    id CHAR(12) PRIMARY KEY DEFAULT generate_forgiven_no_shows_id(),
    booking_id CHAR(12) NOT NULL UNIQUE REFERENCES bookings(id) ON DELETE CASCADE,
    forgiven_by CHAR(12),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_forgiven_no_shows_booking_id ON forgiven_no_shows(booking_id);


-- =======================================
-- PAYMENTS TABLE
-- =======================================
-- PAYMENTS TABLE
-- =======================================
CREATE SEQUENCE invoice_number_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;

CREATE TABLE payments (
    id CHAR(12) PRIMARY KEY DEFAULT generate_payment_id(),
    booking_id CHAR(12) REFERENCES bookings(id) ON DELETE RESTRICT,
    payment_type payment_type NOT NULL,
    payment_method payment_method NOT NULL,
    vendor_code TEXT,
    invoice_number INT UNIQUE DEFAULT nextval('invoice_number_seq'),
    status payment_status NOT NULL DEFAULT 'pending',
    amount NUMERIC(10,2) NOT NULL,
    -- VAT fields (NULL until VAT registered - likely won't need but good to be ready for)
    is_vat_applicable BOOLEAN NOT NULL DEFAULT FALSE,
    vat_rate NUMERIC(5,2),
    vat_amount NUMERIC(10,2),
    net_amount NUMERIC(10,2),
    user_saved_card_id CHAR(12),
    square_payment_id TEXT,
    square_deposit_id CHAR(12),
    idempotency_key VARCHAR(64) UNIQUE,
    fees NUMERIC(10,2) DEFAULT 0,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by CHAR(12),
    gift_card_id CHAR(12)
);

CREATE INDEX idx_payments_bookingid ON payments(booking_id);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_createdat ON payments(created_at);
CREATE INDEX idx_payments_booking_id_status ON payments(booking_id, status);
CREATE INDEX IF NOT EXISTS idx_payments_booking_status_created ON payments(booking_id, status, created_at);
CREATE INDEX idx_payments_created_at_status ON payments(created_at, status);
CREATE INDEX idx_payments_payment_method ON payments(payment_method);
CREATE INDEX idx_payments_payment_method_created_at ON payments(payment_method, created_at);

-- =======================================
-- LOYALTY REDEMPTIONS TABLE
-- =======================================
CREATE TABLE loyalty_redemptions (
    id CHAR(12) PRIMARY KEY DEFAULT generate_loyalty_redemptions_id(),
    user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    stamps_redeemed INT NOT NULL DEFAULT 10,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    applied_to_booking_id CHAR(12) REFERENCES bookings(id) ON DELETE SET NULL,
    redeemed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    applied_at TIMESTAMPTZ,
    expires_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '6 months')
);

CREATE INDEX idx_loyalty_redemptions_user ON loyalty_redemptions(user_id);
CREATE INDEX idx_loyalty_redemptions_status ON loyalty_redemptions(status);

-- =======================================
-- DISCOUNT CAMPAIGNS TABLE
-- =======================================
CREATE TABLE discount_campaigns (
    id CHAR(12) PRIMARY KEY DEFAULT generate_discount_campaigns_id(),
    name VARCHAR(100) NOT NULL,
    description TEXT,
    campaign_type campaign_type NOT NULL DEFAULT 'time_based',
    discount_percent NUMERIC(5,2) NOT NULL,
    scope discount_campaign_scope,
    start_date TIMESTAMPTZ,
    end_date TIMESTAMPTZ,
    milestone_type milestone_type,
    milestone_value INT,
    milestone_unit milestone_unit,
    status discount_campaign_status NOT NULL DEFAULT 'draft',
    max_redemptions INT,
    times_redeemed INT NOT NULL DEFAULT 0,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    CONSTRAINT chk_dates CHECK (campaign_type = 'milestone' OR (start_date IS NOT NULL AND end_date IS NOT NULL AND end_date > start_date)),
    CONSTRAINT chk_discount CHECK (discount_percent > 0 AND discount_percent <= 100),
    CONSTRAINT chk_milestone CHECK (campaign_type = 'time_based' OR (milestone_type IS NOT NULL AND milestone_value IS NOT NULL AND (milestone_type != 'anniversary' OR milestone_unit IS NOT NULL)))
);

CREATE INDEX idx_discount_campaigns_dates ON discount_campaigns(start_date, end_date) WHERE start_date IS NOT NULL;
CREATE INDEX idx_discount_campaigns_status ON discount_campaigns(status);
CREATE INDEX idx_discount_campaigns_type ON discount_campaigns(campaign_type);

-- =======================================
-- BOOKING DISCOUNTS TABLE
-- =======================================
CREATE TABLE booking_discounts (
    id CHAR(12) PRIMARY KEY DEFAULT generate_booking_discounts_id(),
    booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
    user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    discount_source VARCHAR(30) NOT NULL,
    source_id CHAR(12),
    campaign_type campaign_type,
    milestone_type milestone_type,
    discount_percent NUMERIC(5,2) NOT NULL,
    original_total NUMERIC(10,2) NOT NULL,
    discount_amount NUMERIC(10,2) NOT NULL,
    applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_booking_discounts_booking ON booking_discounts(booking_id);
CREATE INDEX idx_booking_discounts_source ON booking_discounts(discount_source, source_id);
CREATE INDEX idx_booking_discounts_user ON booking_discounts(user_id);
CREATE INDEX idx_booking_discounts_milestone ON booking_discounts(user_id, milestone_type, source_id);
CREATE INDEX idx_booking_discounts_booking_source ON booking_discounts(booking_id, discount_source);

-- =======================================
-- BUSINESS SETTINGS TABLE (FOR COMPLIANCE)
-- Stores legal business info for receipts, VAT status, currency, etc.
-- =======================================

CREATE TABLE business_settings (
    id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    business_name VARCHAR(255) NOT NULL,
    business_address TEXT NOT NULL,
    business_phone VARCHAR(20),
    business_email VARCHAR(255),
    vat_registration_number VARCHAR(20),
    is_vat_registered BOOLEAN NOT NULL DEFAULT FALSE,
    default_vat_rate NUMERIC(5,2) NOT NULL DEFAULT 20.00,
    currency_code CHAR(3) NOT NULL DEFAULT 'GBP',
    website_url TEXT,
    gift_card_expiry_months INT NOT NULL DEFAULT 12,
    voucher_type VARCHAR(3) NOT NULL DEFAULT 'SPV',
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Add trigger to auto-update timestamp
CREATE OR REPLACE FUNCTION update_business_settings_timestamp()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_update_business_settings_timestamp
    BEFORE UPDATE ON business_settings
    FOR EACH ROW
    EXECUTE FUNCTION update_business_settings_timestamp();

CREATE TRIGGER trigger_update_discount_campaigns_timestamp
    BEFORE UPDATE ON discount_campaigns
    FOR EACH ROW
    EXECUTE FUNCTION update_business_settings_timestamp();

-- Insert default row
INSERT INTO business_settings (
    business_name,
    business_address,
    business_phone,
    business_email,
    vat_registration_number,
    is_vat_registered,
    default_vat_rate,
    currency_code,
    website_url
) VALUES (
    'Crussell Nail Art Studio',
    'Address',
    '+44 131 123 4567',
    'crussellnails@gmail.com',
    NULL,  -- Set this when we register for VAT
    FALSE, -- Set to TRUE when we register for VAT
    20.00,
    'GBP',
    'https://www.website.co.uk'
);

CREATE TYPE admin_notification_reason AS ENUM ('pending_booking', 'cancelled_booking', 'rescheduled_booking', '1_week_no_pay', '1_month_no_pay', 'affiliate_claim', 'late_cancellation', 'deposit_paid', 'edit_request', 'edit_requested', 'new_booking', 'deposit_not_paid_by_deadline');

CREATE TABLE admin_notifications (
    id CHAR(12) PRIMARY KEY DEFAULT generate_admin_notifications_id(),
    reason admin_notification_reason NOT NULL,
    booking_id CHAR(12) REFERENCES bookings(id),
    user_id CHAR(12) REFERENCES users(id),
    acknowledged_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_admin_notifications_reason ON admin_notifications(reason);
CREATE INDEX idx_admin_notifications_acknowledged_at ON admin_notifications(acknowledged_at);

-- User notification preferences for future user notification system
CREATE TABLE user_notification_preferences (
    id CHAR(12) PRIMARY KEY DEFAULT generate_user_notification_preferences_id(),
    user_id CHAR(12) REFERENCES users(id) ON DELETE CASCADE,
    email_enabled BOOLEAN DEFAULT false,
    sms_enabled BOOLEAN DEFAULT false,
    browser_push_enabled BOOLEAN DEFAULT false,
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_user_notification_preferences_user_id ON user_notification_preferences(user_id);

create table images (
    id CHAR(12) PRIMARY KEY DEFAULT generate_images_id(),
    url text not null,
    thumbnail_url text not null,
    tag_names text[] not null default '{}',
    created_at timestamptz not null default now(),
    full_avif_url text,
    full_webp_url text,
    full_jpg_url text,
    full_jxl_url text,
    thumb_avif_url text,
    thumb_webp_url text,
    thumb_jpg_url text
);

-- only for autocomplete text
create table tags (
    id CHAR(12) PRIMARY KEY DEFAULT generate_tags_id(),
    name text not null unique
);

-- Indexes
create index idx_images_tag_names on images using gin(tag_names);
-- pg_trgm GIN index not applicable to tag_names (text[] array).
-- For ILIKE search on unnested tags at scale, normalize into a separate image_tags table
-- and add: CREATE INDEX idx_image_tags_name_trgm ON image_tags USING GIN (name gin_trgm_ops);
create index idx_images_created_at on images(created_at desc);
CREATE INDEX IF NOT EXISTS idx_images_url_trgm ON images USING GIN (url gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_images_thumbnail_url_trgm ON images USING GIN (thumbnail_url gin_trgm_ops);
create index idx_tags_name_trgm on tags using gin (name gin_trgm_ops);

-- =======================================
-- GDPR COMPLIANCE FUNCTIONS
-- =======================================

/*
GDPR COMPLIANCE NOTES:
- Legal basis for booking data: Contract performance (Art 6(1)(b) GDPR)
- Legal basis for optional data (marketing): Consent (Art 6(1)(a) GDPR)
- Data retention: While consent remains and account is active
- Right to be forgotten: anonymize_user() for registered users
- Subject access rights: export_all_user_data() provides complete export
*/

-- Anonymize registered user (Right to be Forgotten)
-- WHY: GDPR Article 17 - users can request data deletion
-- WHEN: User requests account deletion
-- OUTPUT: Converts personal data to anonymous placeholder
-- NOTE: phone/date_of_birth are NOT NULL so we use placeholder values, not NULL
CREATE OR REPLACE FUNCTION anonymize_user(target_id CHAR(12))
RETURNS VOID AS $$
BEGIN
    UPDATE users
    SET
        n_first_name          = 'Deleted',
        n_last_name           = 'User',
        email                 = CONCAT('deleted+', target_id, '@deleted.invalid'),
        phone                 = '+000000000000',          -- NOT NULL column: use placeholder
        date_of_birth         = '1900-01-01',             -- NOT NULL column: use placeholder
        profile_pic_url       = NULL,
        account_role          = 'guest',
        loyalty_stamps        = 0,
        referral_code         = NULL,
        data_retention_consent = FALSE,
        data_consent_updated_at = NOW(),
        updated_at            = NOW(),
        password_hash         = NULL
    WHERE id = target_id
      AND account_role != 'guest';

    -- Scrub social login identities (immutable_id is PII from OAuth providers)
    DELETE FROM user_social_logins WHERE user_id = target_id;

    -- Soft-delete all saved cards and clear PCI data
    UPDATE user_saved_cards
    SET deleted_at = NOW(),
        retained_until = NOW() + INTERVAL '7 years',
        last_4 = 'XXXX',
        fingerprint = NULL,
        exp_month = 1,
        exp_year = 2000
    WHERE user_id = target_id;

    -- Expire all pending verification codes
    UPDATE verification_codes
    SET used_at = NOW()
    WHERE user_id = target_id
      AND used_at IS NULL;

    -- Scrub RESERVATION entries in time_blockers that reference this user
    UPDATE time_blockers
    SET description = NULL
    WHERE created_by = target_id
      AND (description LIKE 'RESERVATION:user:%'
        OR description LIKE 'RESERVATION:edit_request:%');

    -- Scrub notes on booking_edit_requests made by this user (free-text PII)
    UPDATE booking_edit_requests
    SET notes = NULL
    WHERE requested_by = target_id;

    -- Clear notification preferences (no contractual basis after account closure)
    DELETE FROM user_notification_preferences WHERE user_id = target_id;

    -- Anonymize patch test records (medical-adjacent PII — unlink user, preserve test history)
    UPDATE user_patch_tests SET user_id = NULL WHERE user_id = target_id;

    -- Anonymize referral relationships (unlink this user's side, preserve the other party's record)
    UPDATE user_referrals SET referrer_id = NULL WHERE referrer_id = target_id;
    UPDATE user_referrals SET referred_id = NULL WHERE referred_id = target_id;

    -- Anonymize admin notification references (not customer data — just drop the user link)
    UPDATE admin_notifications SET user_id = NULL WHERE user_id = target_id;

    -- Clear login audit trail (no ongoing legal basis after account closure)
    DELETE FROM login_audit WHERE user_id = target_id;
END;
$$ LANGUAGE plpgsql;

-- Delete guest user completely (no contractual retention basis)
-- WHY: Guest accounts have no ongoing contractual or legal basis for retention
-- WHEN: User requests deletion or GDPR cleanup
-- OUTPUT: Full removal of guest account from users table
-- SAFETY: FK constraints are SET NULL on financial tables — records survive deletion.
--         Saved cards are soft-deleted with 7-year retention before unlinking.
CREATE OR REPLACE FUNCTION delete_guest_user(target_id CHAR(12))
RETURNS VOID AS $$
BEGIN
    UPDATE user_saved_cards
    SET deleted_at = NOW(),
        retained_until = NOW() + INTERVAL '7 years',
        last_4 = 'XXXX',
        fingerprint = NULL,
        exp_month = 1,
        exp_year = 2000,
        user_id = NULL
    WHERE user_id = target_id;

    -- Clear login audit trail before deleting the user row
    DELETE FROM login_audit WHERE user_id = target_id;

    DELETE FROM users WHERE id = target_id AND account_role = 'guest';
END;
$$ LANGUAGE plpgsql;

-- Complete Subject Access Request Export
-- WHY: GDPR Article 15 - users have right to access their data
-- WHEN: Customer requests "what data do you have on me?"
-- OUTPUT: Complete JSON export of all user data
-- USE: Call from Go API endpoint, return to user via email/download
CREATE OR REPLACE FUNCTION export_all_user_data(target_user_id CHAR(12))
RETURNS JSON AS $$
DECLARE
    result JSON;
BEGIN
    SELECT json_build_object(
        'user_profile', (
            SELECT json_build_object(
                'id',                                id,
                'first_name',                        n_first_name,
                'last_name',                         n_last_name,
                'full_name',                         fn,
                'email',                             email,
                'phone',                             phone,
                'date_of_birth',                     date_of_birth,
                'profile_pic_url',                   profile_pic_url,
                'account_role',                      account_role,
                'account_type',                      account_type,
                'last_login_at',                     last_login_at,
                'loyalty_stamps',                    loyalty_stamps,
                'deposits_required',                 deposits_required,
                'referral_code',                     referral_code,
                'privacy_policy_and_terms_consent',  privacy_policy_and_terms_consent,
                'policy_consent_updated_at',         policy_consent_updated_at,
                'data_retention_consent',            data_retention_consent,
                'data_consent_updated_at',           data_consent_updated_at,
                'created_at',                        created_at,
                'updated_at',                        updated_at,
                'failed_attempts',                   failed_attempts,
                'locked_until',                      locked_until
            )
            FROM users WHERE id = target_user_id
        ),
        'bookings', (
            SELECT COALESCE(json_agg(
                json_build_object(
                    'booking_id',  b.id,
                    'start_time',  b.start_time,
                    'status',      b.status,
                    'notes',       b.notes,
                    'created_by',  b.created_by,
                    'created_at',  b.created_at,
                    'updated_at',  b.updated_at,
                    'total_price', (
                        SELECT COALESCE(SUM(price_val), 0) FROM (
                            SELECT COALESCE(bsvc2.override_price, s2.price) AS price_val
                            FROM booking_services bsvc2
                            JOIN services s2 ON bsvc2.service_id = s2.id
                            WHERE bsvc2.booking_id = b.id
                            UNION ALL
                            SELECT COALESCE(bcs2.override_price, cs2.price)
                            FROM booking_custom_services bcs2
                            JOIN custom_services cs2 ON bcs2.custom_service_id = cs2.id
                            WHERE bcs2.booking_id = b.id
                        ) price_sub
                    ),
                    'services', (
                        SELECT COALESCE(json_agg(service_obj), '[]'::json) FROM (
                            SELECT json_build_object(
                                'service_id',        s.id,
                                'name',              s.name,
                                'description',       s.description,
                                'price',             COALESCE(bsvc.override_price, s.price),
                                'duration_minutes',  COALESCE(bsvc.override_duration_minutes, s.duration_minutes)
                            ) AS service_obj
                            FROM booking_services bsvc
                            JOIN services s ON bsvc.service_id = s.id
                            WHERE bsvc.booking_id = b.id
                            UNION ALL
                            SELECT json_build_object(
                                'service_id',        cs.id,
                                'name',              cs.name,
                                'description',       cs.description,
                                'price',             COALESCE(bcs.override_price, cs.price),
                                'duration_minutes',  COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
                            )
                            FROM booking_custom_services bcs
                            JOIN custom_services cs ON bcs.custom_service_id = cs.id
                            WHERE bcs.booking_id = b.id
                        ) services_sub
                    )
                )
            ORDER BY b.start_time DESC), '[]'::json)
            FROM bookings b WHERE b.user_id = target_user_id
        ),
        'payments', (
            SELECT COALESCE(json_agg(
                json_build_object(
                    'payment_id',     p.id,
                    'booking_id',     p.booking_id,
                    'payment_type',   p.payment_type,
                    'payment_method', p.payment_method,
                    'vendor_code',    p.vendor_code,
                    'invoice_number', p.invoice_number,
                    'status',         p.status,
                    'amount',         p.amount,
                    'vat_amount',     p.vat_amount,
                    'net_amount',     p.net_amount,
                    'created_at',     p.created_at,
                    'updated_at',     p.updated_at
                )
            ORDER BY p.created_at DESC), '[]'::json)
            FROM payments p
            JOIN bookings b ON p.booking_id = b.id
            WHERE b.user_id = target_user_id
        ),
        'patch_tests', (
            SELECT COALESCE(json_agg(
                json_build_object(
                    'patch_test_id',   upt.patch_test_id,
                    'name',            pt.name,
                    'description',     pt.description,
                    'tested_at',       upt.tested_at
                )
            ORDER BY upt.tested_at DESC), '[]'::json)
            FROM user_patch_tests upt
            JOIN patch_tests pt ON upt.patch_test_id = pt.id
            WHERE upt.user_id = target_user_id
        ),
        'referrals', (
            SELECT json_build_object(
                'referred_by', (
                    SELECT json_build_object(
                        'referrer_id',         ur.referrer_id,
                        'referrer_name',       u.fn,
                        'referred_at',         ur.referred_at
                    )
                    FROM user_referrals ur
                    JOIN users u ON u.id = ur.referrer_id
                    WHERE ur.referred_id = target_user_id
                    LIMIT 1
                ),
                'referred_users', (
                    SELECT COALESCE(json_agg(
                        json_build_object(
                            'referred_id',   ur.referred_id,
                            'referred_name', u.fn,
                            'referred_at',   ur.referred_at
                        )
                    ORDER BY ur.referred_at DESC), '[]'::json)
                    FROM user_referrals ur
                    JOIN users u ON u.id = ur.referred_id
                    WHERE ur.referrer_id = target_user_id
                )
            )
        ),
        'referral_discounts', (
            SELECT COALESCE(json_agg(json_build_object(
                'id',               rd.id,
                'discount_percent', rd.discount_percent,
                'used',             rd.used,
                'created_at',       rd.created_at,
                'used_at',          rd.used_at
            ) ORDER BY rd.created_at DESC), '[]'::json)
            FROM referral_discounts rd WHERE rd.user_id = target_user_id
        ),
        'notification_preferences', (
            SELECT COALESCE(json_agg(
                json_build_object(
                    'email_enabled',        unp.email_enabled,
                    'sms_enabled',          unp.sms_enabled,
                    'browser_push_enabled', unp.browser_push_enabled,
                    'updated_at',           unp.updated_at
                )
            ), '[]'::json)
            FROM user_notification_preferences unp
            WHERE unp.user_id = target_user_id
        ),
        'saved_cards', (
            SELECT COALESCE(json_agg(json_build_object(
                'card_id',         id,
                'brand',           brand,
                'last_4',          last_4,
                'exp_month',       exp_month,
                'exp_year',        exp_year,
                'fingerprint',     fingerprint,
                'is_default',      is_default,
                'deleted_at',      deleted_at,
                'retained_until',  retained_until,
                'created_at',      created_at
            ) ORDER BY created_at DESC), '[]'::json)
            FROM user_saved_cards WHERE user_id = target_user_id
        ),
        'refunds', (
            SELECT COALESCE(json_agg(json_build_object(
                'refund_id',      r.id,
                'payment_id',     r.payment_id,
                'booking_id',     r.booking_id,
                'amount',         r.amount,
                'status',         r.status,
                'reason',         r.reason,
                'created_at',     r.created_at
            ) ORDER BY r.created_at DESC), '[]'::json)
            FROM refunds r
            JOIN payments p ON r.payment_id = p.id
            JOIN bookings b ON p.booking_id = b.id
            WHERE b.user_id = target_user_id
        ),
        'social_logins', (
            SELECT COALESCE(json_agg(json_build_object(
                'provider',    usl.provider,
                'created_at',  usl.created_at
            ) ORDER BY usl.created_at ASC), '[]'::json)
            FROM user_social_logins usl WHERE usl.user_id = target_user_id
        ),
        'loyalty_redemptions', (
            SELECT COALESCE(json_agg(json_build_object(
                'id',                  lr.id,
                'stamps_redeemed',     lr.stamps_redeemed,
                'status',              lr.status,
                'booking_date',        b.start_time,
                'booking_services',    (
                    SELECT COALESCE(string_agg(name, ', '), '') FROM (
                        SELECT s.name
                        FROM booking_services bsvc
                        JOIN services s ON bsvc.service_id = s.id
                        WHERE bsvc.booking_id = lr.applied_to_booking_id
                        UNION ALL
                        SELECT cs.name
                        FROM booking_custom_services bcs
                        JOIN custom_services cs ON bcs.custom_service_id = cs.id
                        WHERE bcs.booking_id = lr.applied_to_booking_id
                    ) svc_names
                ),
                'redeemed_at',         lr.redeemed_at,
                'applied_at',          lr.applied_at,
                'expires_at',          lr.expires_at
            ) ORDER BY lr.redeemed_at DESC), '[]'::json)
            FROM loyalty_redemptions lr
            LEFT JOIN bookings b ON lr.applied_to_booking_id = b.id
            WHERE lr.user_id = target_user_id
        ),
        'booking_discounts', (
            SELECT COALESCE(json_agg(json_build_object(
                'id',               bd.id,
                'booking_id',       bd.booking_id,
                'campaign_name',    COALESCE(dc.name, bd.discount_source),
                'discount_source',  bd.discount_source,
                'source_id',        bd.source_id,
                'campaign_type',    bd.campaign_type,
                'milestone_type',   bd.milestone_type,
                'discount_percent', bd.discount_percent,
                'original_total',   bd.original_total,
                'discount_amount',  bd.discount_amount,
                'applied_at',       bd.applied_at
            ) ORDER BY bd.applied_at DESC), '[]'::json)
            FROM booking_discounts bd
            LEFT JOIN discount_campaigns dc ON bd.source_id = dc.id AND bd.discount_source = 'campaign'
            WHERE bd.user_id = target_user_id
        ),
        'edit_requests', (
            SELECT COALESCE(json_agg(json_build_object(
                'id',             ber.id,
                'booking_id',     ber.booking_id,
                'new_start_time', ber.new_start_time,
                'new_services',   ber.new_services,
                'notes',          ber.notes,
                'has_overrides',  ber.has_overrides,
                'updated_at',     ber.updated_at
            ) ORDER BY ber.updated_at DESC), '[]'::json)
            FROM booking_edit_requests ber WHERE ber.requested_by = target_user_id
        ),
        'affiliate_payouts', (
            SELECT COALESCE(json_agg(json_build_object(
                'id',         ap.id,
                'amount',     ap.amount,
                'status',     ap.status,
                'created_at', ap.created_at
            ) ORDER BY ap.created_at DESC), '[]'::json)
            FROM affiliate_payouts ap WHERE ap.affiliate_id = target_user_id
        ),
        'forgiven_no_shows', (
            SELECT COALESCE(json_agg(json_build_object(
                'id',         fns.id,
                'booking_id', fns.booking_id,
                'created_at', fns.created_at
            ) ORDER BY fns.created_at DESC), '[]'::json)
            FROM forgiven_no_shows fns
            JOIN bookings b ON fns.booking_id = b.id
            WHERE b.user_id = target_user_id
        ),
        'gift_card_balance', (
            SELECT COALESCE(
                (SELECT json_build_object(
                    'balance',    COALESCE(ugb.balance, 0),
                    'updated_at', ugb.updated_at
                ) FROM user_giftcard_balances ugb WHERE ugb.user_id = target_user_id),
                json_build_object('balance', 0, 'updated_at', NULL)
            )
        ),
        'gift_card_transactions', (
            SELECT COALESCE(json_agg(json_build_object(
                'id',                gct.id,
                'transaction_type',  gct.transaction_type,
                'amount',            gct.amount,
                'reference_type',    gct.reference_type,
                'reference_id',      gct.reference_id,
                'notes',             gct.notes,
                'created_at',        gct.created_at
            ) ORDER BY gct.created_at DESC), '[]'::json)
            FROM gift_card_transactions gct
            WHERE gct.user_id = target_user_id
        ),
        'gift_cards', (
            SELECT COALESCE(json_agg(json_build_object(
                'id',                gc.id,
                'total_funds_added', gc.total_funds_added,
                'amount_remaining',  gc.amount_remaining,
                'expiry_date',       gc.expiry_date,
                'redeemed_at',       gc.redeemed_at,
                'created_at',        gc.created_at
            ) ORDER BY gc.created_at DESC), '[]'::json)
            FROM gift_cards gc WHERE gc.redeemed_by = target_user_id
        ),
        'admin_audit_log', (
            SELECT COALESCE(json_agg(json_build_object(
                'action_type', aal.action_type,
                'details',     aal.details,
                'created_at',  aal.created_at
            ) ORDER BY aal.created_at DESC), '[]'::json)
            FROM admin_audit_log aal WHERE aal.target_user_id = export_all_user_data.target_user_id
        ),
        'login_audit', (
            SELECT COALESCE(json_agg(json_build_object(
                'attempt_type',  la.attempt_type,
                'ip_address',    la.ip_address::text,
                'success',       la.success,
                'created_at',    la.created_at
            ) ORDER BY la.created_at DESC), '[]'::json)
            FROM login_audit la WHERE la.user_id = target_user_id
        ),
        'refresh_tokens', (
            SELECT COALESCE(json_agg(json_build_object(
                'role',          rt.role,
                'revoked',       rt.revoked,
                'created_at',    rt.created_at,
                'expires_at',    rt.expires_at
            ) ORDER BY rt.created_at DESC), '[]'::json)
            FROM refresh_tokens rt WHERE rt.user_id = target_user_id
        ),
        'name_history', (
            SELECT COALESCE(json_agg(json_build_object(
                'previous_first_name', nh.previous_first_name,
                'previous_last_name',  nh.previous_last_name,
                'booking_id',          nh.booking_id,
                'changed_at',          nh.changed_at
            ) ORDER BY nh.changed_at DESC), '[]'::json)
            FROM name_history nh WHERE nh.user_id = target_user_id
        ),
        'export_metadata', json_build_object(
            'exported_at',    NOW(),
            'exported_by',    'system',
            'user_id',        target_user_id,
            'format_version', '1.0'
        )
    ) INTO result;

    RETURN result;
END;
$$ LANGUAGE plpgsql;

-- =======================================
-- TAX COMPLIANCE FUNCTIONS
-- =======================================

/*
TAX COMPLIANCE NOTES:
- Below £90k turnover: No VAT registration required
- Above £90k turnover: VAT registration mandatory, Making Tax Digital required
- All businesses: Income tax records required (simplified from April 2026/2027)
- VAT rate: 20% standard rate applies to nail services
*/

-- VAT Return Data (for Making Tax Digital submission)
-- WHY: VAT registered businesses must submit quarterly VAT returns via MTD software
-- WHEN: Every quarter if VAT registered (£90k+ annual turnover)
-- OUTPUT: Summary totals for VAT return - total sales, VAT charged, net sales
-- USE: Export to QuickBooks/Xero for MTD submission to HMRC
CREATE OR REPLACE FUNCTION get_vat_return_data(
    start_date DATE,
    end_date   DATE
)
RETURNS TABLE (
    period_start       DATE,
    period_end         DATE,
    total_sales        NUMERIC(12,2),
    total_vat_charged  NUMERIC(12,2),
    net_sales          NUMERIC(12,2),
    transaction_count  BIGINT
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        start_date AS period_start,
        end_date   AS period_end,
        COALESCE(SUM(total_sales), 0) AS total_sales,
        COALESCE(SUM(total_vat_charged), 0) AS total_vat_charged,
        COALESCE(SUM(net_sales), 0) AS net_sales,
        COALESCE(SUM(transaction_count), 0)::BIGINT AS transaction_count
    FROM (
        SELECT
            COALESCE(SUM(p.amount), 0) AS total_sales,
            COALESCE(SUM(
                CASE
                    WHEN p.vat_amount IS NOT NULL THEN p.vat_amount
                    WHEN (SELECT is_vat_registered FROM business_settings WHERE id = 1) THEN
                        ROUND(p.amount - (p.amount / (1 + COALESCE(p.vat_rate,
                            (SELECT default_vat_rate FROM business_settings WHERE id = 1)) / 100)), 2)
                    ELSE 0
                END
            ), 0) AS total_vat_charged,
            COALESCE(SUM(
                CASE
                    WHEN p.net_amount IS NOT NULL THEN p.net_amount
                    WHEN (SELECT is_vat_registered FROM business_settings WHERE id = 1) THEN
                        ROUND(p.amount / (1 + COALESCE(p.vat_rate,
                            (SELECT default_vat_rate FROM business_settings WHERE id = 1)) / 100), 2)
                    ELSE p.amount
                END
            ), 0) AS net_sales,
            COUNT(*)::BIGINT AS transaction_count
        FROM payments p
        JOIN bookings b ON p.booking_id = b.id
        WHERE p.status = 'completed'
          AND p.created_at::date BETWEEN start_date AND end_date
          AND p.payment_type IN ('full', 'partial', 'balance')

        UNION ALL

        SELECT
            COALESCE(SUM(ts.total_amount), 0) AS total_sales,
            COALESCE(SUM(COALESCE(ts.vat_amount, 0)), 0) AS total_vat_charged,
            COALESCE(SUM(COALESCE(ts.net_amount, ts.total_amount)), 0) AS net_sales,
            COUNT(*)::BIGINT AS transaction_count
        FROM till_sales ts
        WHERE ts.status = 'completed'
          AND ts.payment_method != 'on_the_house'
          AND ts.created_at::date BETWEEN start_date AND end_date
    ) sub;
END;
$$ LANGUAGE plpgsql;

-- Detailed Transaction Export (Primary tax function)
-- WHY: All businesses need transaction records for tax returns and accounting
-- WHEN: Monthly/quarterly for accounting software import
-- OUTPUT: CSV-compatible transaction list with customer, service, payment details
-- USE: Import directly into QuickBooks, Xero, or other accounting software
-- NOTE: include_vat=false (default for micro businesses); set true only when VAT registered
CREATE OR REPLACE FUNCTION export_sales_transactions(
    start_date  DATE,
    end_date    DATE,
    include_vat BOOLEAN DEFAULT FALSE
)
RETURNS TABLE (
    transaction_date  DATE,
    invoice_number    TEXT,
    customer_name     TEXT,
    customer_email    TEXT,
    service_description TEXT,
    payment_method    TEXT,
    gross_amount      NUMERIC(10,2),
    net_amount        NUMERIC(10,2),
    vat_amount        NUMERIC(10,2),
    vat_rate          NUMERIC(5,2),
    booking_id        CHAR(12),
    payment_id        CHAR(12)
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        p.created_at::date AS transaction_date,
        p.invoice_number::text AS invoice_number,
        COALESCE(
            CASE WHEN u.n_first_name = 'Deleted' THEN NULL ELSE u.fn END,
            'Walk-in Customer'
        ) AS customer_name,
        CASE WHEN u.n_first_name = 'Deleted' THEN NULL ELSE u.email END AS customer_email,
        string_agg(COALESCE(s.name, cs.name), ', ' ORDER BY COALESCE(s.name, cs.name)) AS service_description,
        p.payment_method::text AS payment_method,
        p.amount AS gross_amount,
        CASE
            WHEN include_vat AND p.net_amount IS NOT NULL THEN p.net_amount
            WHEN include_vat THEN COALESCE(p.net_amount, ROUND(p.amount / (1 + COALESCE(p.vat_rate, (SELECT default_vat_rate FROM business_settings)) / 100), 2))
            ELSE p.amount
        END AS net_amount,
        CASE
            WHEN include_vat AND p.vat_amount IS NOT NULL THEN p.vat_amount
            WHEN include_vat THEN COALESCE(p.vat_amount, ROUND(p.amount - (p.amount / (1 + COALESCE(p.vat_rate, (SELECT default_vat_rate FROM business_settings)) / 100)), 2))
            ELSE 0
        END AS vat_amount,
        CASE
            WHEN include_vat THEN COALESCE(p.vat_rate, 20.00)
            ELSE NULL
        END AS vat_rate,
        b.id AS booking_id,
        p.id AS payment_id
    FROM payments p
    JOIN bookings b ON p.booking_id = b.id
    LEFT JOIN users u ON b.user_id = u.id
    LEFT JOIN booking_services bsvc ON b.id = bsvc.booking_id
    LEFT JOIN services s ON bsvc.service_id = s.id
    LEFT JOIN booking_custom_services bcs ON b.id = bcs.booking_id
    LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
    WHERE p.status = 'completed'
      AND p.created_at::date BETWEEN start_date AND end_date
      AND p.payment_type IN ('full', 'partial', 'balance')
    GROUP BY p.id, b.id, u.fn, u.n_first_name, u.email,
             p.created_at, p.amount, p.net_amount, p.vat_amount,
             p.vat_rate, p.payment_method, p.invoice_number
    ORDER BY p.created_at;
END;
$$ LANGUAGE plpgsql;

-- Monthly Business Summary
-- WHY: Track business performance trends and payment method preferences
-- WHEN: Monthly review of business performance
-- OUTPUT: Month-by-month breakdown of revenue, bookings, payment methods
-- USE: Personal business insights - "How am I doing vs last month?"
CREATE OR REPLACE FUNCTION get_monthly_business_summary(
    start_date DATE,
    end_date   DATE
)
RETURNS TABLE (
    month_year        TEXT,
    total_bookings    BIGINT,
    total_revenue     NUMERIC(12,2),
    cash_payments     NUMERIC(12,2),
    card_payments     NUMERIC(12,2),
    online_payments   NUMERIC(12,2),
    avg_transaction   NUMERIC(10,2)
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        TO_CHAR(p.created_at, 'YYYY-MM') AS month_year,
        COUNT(DISTINCT b.id) AS total_bookings,
        SUM(p.amount) AS total_revenue,
        SUM(CASE WHEN p.payment_method = 'cash'          THEN p.amount ELSE 0 END) AS cash_payments,
        SUM(CASE WHEN p.payment_method = 'in_person_card' THEN p.amount ELSE 0 END) AS card_payments,
        SUM(CASE WHEN p.payment_method = 'online_square'  THEN p.amount ELSE 0 END) AS online_payments,
        ROUND(AVG(p.amount), 2) AS avg_transaction
    FROM payments p
    JOIN bookings b ON p.booking_id = b.id
    WHERE p.status = 'completed'
      AND p.created_at::date BETWEEN start_date AND end_date
      AND p.payment_type IN ('full', 'partial', 'balance')
    GROUP BY TO_CHAR(p.created_at, 'YYYY-MM')
    ORDER BY month_year;
END;
$$ LANGUAGE plpgsql;

-- Simple Sales Totals (Quick business snapshot)
-- WHY: Quick overview of current performance for dashboard/API
-- WHEN: Dashboard loading, daily/weekly check-ins
-- OUTPUT: Total sales, transaction count, payment method breakdown
-- USE: Homepage dashboard, "How much did I make this week?" queries
CREATE OR REPLACE FUNCTION get_sales_totals(
    start_date DATE,
    end_date   DATE
)
RETURNS TABLE (
    total_sales        NUMERIC(12,2),
    total_transactions BIGINT,
    cash_total         NUMERIC(12,2),
    card_total         NUMERIC(12,2),
    online_total       NUMERIC(12,2)
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        COALESCE(SUM(p.amount), 0) AS total_sales,
        COUNT(*) AS total_transactions,
        COALESCE(SUM(CASE WHEN p.payment_method = 'cash'           THEN p.amount ELSE 0 END), 0) AS cash_total,
        COALESCE(SUM(CASE WHEN p.payment_method = 'in_person_card'  THEN p.amount ELSE 0 END), 0) AS card_total,
        COALESCE(SUM(CASE WHEN p.payment_method = 'online_square'   THEN p.amount ELSE 0 END), 0) AS online_total
    FROM payments p
    JOIN bookings b ON p.booking_id = b.id
    WHERE p.status = 'completed'
      AND p.created_at::date BETWEEN start_date AND end_date
      AND p.payment_type IN ('full', 'partial', 'balance');
END;
$$ LANGUAGE plpgsql;

-- =======================================
-- UTILITY FUNCTIONS
-- =======================================

-- VAT Registration Transition Function
-- WHY: When crossing £90k threshold, must register for VAT and backfill existing data
-- WHEN: One-time use when VAT registration becomes mandatory
-- OUTPUT: Updates all historical payments with VAT breakdown + sets new defaults
-- USE: Call once when we register for VAT - transforms business from non-VAT to VAT
CREATE OR REPLACE FUNCTION enable_vat_registration(
    registration_date DATE    DEFAULT CURRENT_DATE,
    vat_rate          NUMERIC(5,2) DEFAULT 20.00,
    vat_reg_number    TEXT    DEFAULT NULL
)
RETURNS TABLE (
    payments_updated           INT,
    till_sales_updated         INT,
    total_vat_calculated       NUMERIC(12,2),
    total_net_calculated       NUMERIC(12,2),
    registration_effective_date DATE,
    settings_updated           BOOLEAN
) AS $$
DECLARE
    pay_updated_count INT;
    till_updated_count INT;
    total_vat     NUMERIC(12,2);
    total_net     NUMERIC(12,2);
    settings_ok   BOOLEAN := FALSE;
BEGIN
    -- Retrospectively apply VAT to completed payments
    UPDATE payments
    SET
        vat_rate        = enable_vat_registration.vat_rate,
        vat_amount      = ROUND(amount - (amount / (1 + enable_vat_registration.vat_rate / 100)), 2),
        net_amount      = ROUND(amount / (1 + enable_vat_registration.vat_rate / 100), 2),
        is_vat_applicable = TRUE,
        updated_at      = NOW()
    WHERE status = 'completed'
      AND created_at::date >= registration_date
      AND vat_amount IS NULL;

    GET DIAGNOSTICS pay_updated_count = ROW_COUNT;

    -- Retrospectively apply VAT to completed till sales (gift card purchases etc.)
    UPDATE till_sales
    SET
        vat_rate          = enable_vat_registration.vat_rate,
        vat_amount        = ROUND(total_amount - (total_amount / (1 + enable_vat_registration.vat_rate / 100)), 2),
        net_amount        = ROUND(total_amount / (1 + enable_vat_registration.vat_rate / 100), 2),
        is_vat_applicable = TRUE,
        updated_at        = NOW()
    WHERE status = 'completed'
      AND created_at::date >= registration_date
      AND vat_amount IS NULL;

    GET DIAGNOSTICS till_updated_count = ROW_COUNT;

    -- Aggregate totals
    SELECT
        COALESCE(SUM(p.vat_amount), 0),
        COALESCE(SUM(p.net_amount), 0)
    INTO total_vat, total_net
    FROM payments p
    WHERE status = 'completed'
      AND created_at::date >= registration_date
      AND vat_amount IS NOT NULL;

    -- Also include till_sales in the aggregate totals
    SELECT
        total_vat + COALESCE(SUM(ts.vat_amount), 0),
        total_net + COALESCE(SUM(ts.net_amount), 0)
    INTO total_vat, total_net
    FROM till_sales ts
    WHERE status = 'completed'
      AND created_at::date >= registration_date
      AND vat_amount IS NOT NULL;

    -- Update business_settings record
    UPDATE business_settings
    SET
        is_vat_registered       = TRUE,
        vat_registration_number = COALESCE(vat_reg_number, vat_registration_number),
        default_vat_rate        = enable_vat_registration.vat_rate,
        updated_at              = NOW()
    WHERE id = 1;

    IF FOUND THEN
        settings_ok := TRUE;
    END IF;

    RETURN QUERY SELECT
        pay_updated_count,
        till_updated_count,
        total_vat,
        total_net,
        registration_date,
        settings_ok;
END;
$$ LANGUAGE plpgsql;

-- Update payment with VAT (for new payments after VAT registration)
-- WHY: After VAT registration, all new payments need VAT calculation
-- WHEN: Called when processing new payments (if VAT registered)
-- OUTPUT: Updates individual payment with VAT breakdown
-- USE: Call from Go when processing new payments after VAT registration
CREATE OR REPLACE FUNCTION apply_vat_to_payment(
    payment_id CHAR(12),
    vat_rate   NUMERIC(5,2) DEFAULT NULL  -- NULL = use default from business_settings
)
RETURNS VOID AS $$
DECLARE
    effective_rate NUMERIC(5,2);
BEGIN
    IF vat_rate IS NULL THEN
        SELECT default_vat_rate INTO effective_rate
        FROM business_settings
        WHERE id = 1 AND is_vat_registered = TRUE;

        IF NOT FOUND OR effective_rate IS NULL THEN
            RAISE EXCEPTION 'No VAT rate provided and business is not registered for VAT';
        END IF;
    ELSE
        effective_rate := vat_rate;
    END IF;

    UPDATE payments
    SET
        vat_rate          = effective_rate,
        vat_amount        = ROUND(amount - (amount / (1 + effective_rate / 100)), 2),
        net_amount        = ROUND(amount / (1 + effective_rate / 100), 2),
        is_vat_applicable = TRUE,
        updated_at        = NOW()
    WHERE id = payment_id
      AND vat_amount IS NULL;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION apply_vat_to_till_sale(
    sale_id  CHAR(12),
    vat_rate NUMERIC(5,2) DEFAULT NULL
)
RETURNS VOID AS $$
DECLARE
    effective_rate NUMERIC(5,2);
BEGIN
    IF vat_rate IS NULL THEN
        SELECT default_vat_rate INTO effective_rate
        FROM business_settings
        WHERE id = 1 AND is_vat_registered = TRUE;

        IF NOT FOUND OR effective_rate IS NULL THEN
            RAISE EXCEPTION 'No VAT rate provided and business is not registered for VAT';
        END IF;
    ELSE
        effective_rate := vat_rate;
    END IF;

    UPDATE till_sales
    SET
        vat_rate          = effective_rate,
        vat_amount        = ROUND(total_amount - (total_amount / (1 + effective_rate / 100)), 2),
        net_amount        = ROUND(total_amount / (1 + effective_rate / 100), 2),
        is_vat_applicable = TRUE,
        updated_at        = NOW()
    WHERE id = sale_id
      AND vat_amount IS NULL;
END;
$$ LANGUAGE plpgsql;

-- Simple VAT Calculation Helper
-- WHY: Manual VAT calculations or validation
-- WHEN: Checking VAT calculations or manual entry corrections
-- OUTPUT: net amount and vat amount from gross amount
-- USE: Manual calculations, validation, or corrections
CREATE OR REPLACE FUNCTION calculate_vat(
    gross_amount NUMERIC(10,2),
    vat_rate     NUMERIC(5,2) DEFAULT NULL  -- NULL = use default from business_settings
)
RETURNS TABLE(
    net NUMERIC(10,2),
    vat NUMERIC(10,2)
) AS $$
DECLARE
    effective_rate NUMERIC(5,2);
BEGIN
    IF vat_rate IS NULL THEN
        SELECT default_vat_rate INTO effective_rate
        FROM business_settings
        WHERE id = 1 AND is_vat_registered = TRUE;

        IF NOT FOUND OR effective_rate IS NULL THEN
            RAISE EXCEPTION 'No VAT rate provided and business is not registered for VAT';
        END IF;
    ELSE
        effective_rate := vat_rate;
    END IF;

    RETURN QUERY
    SELECT
        ROUND(gross_amount / (1 + effective_rate / 100), 2) AS net,
        gross_amount - ROUND(gross_amount / (1 + effective_rate / 100), 2) AS vat;
END;
$$ LANGUAGE plpgsql;

-- =======================================
-- UK RECEIPT COMPLIANCE FUNCTION
-- =======================================

/*
UK RECEIPT REQUIREMENTS:
- Business name and address
- Date and time of transaction
- Description of goods/services
- Amount charged (including VAT if registered)
- VAT breakdown (if VAT registered): VAT number, rate, VAT amount, net amount
- Receipt/invoice number
- Payment method
- Customer details (if not anonymised)
*/

-- Complete Receipt Data for UK Compliance
-- WHY: Generate legally compliant receipts for customers
-- WHEN: After each completed payment/booking
-- OUTPUT: All data needed for receipt printing/email
-- USE: Call from Go API to generate customer receipts
CREATE OR REPLACE FUNCTION get_receipt_data(p_payment_id CHAR(12))
RETURNS TABLE (
    -- Business Information
    business_name           TEXT,
    business_address        TEXT,
    business_phone          TEXT,
    business_email          TEXT,
    vat_registration_number TEXT,
    is_vat_registered       BOOLEAN,

    -- Payment Information
    payment_id              CHAR(12),
    invoice_number          TEXT,
    transaction_date        TIMESTAMPTZ,
    payment_method          TEXT,
    payment_status          TEXT,

    -- Customer Information
    customer_id             CHAR(12),
    customer_name           TEXT,
    customer_email          TEXT,
    customer_phone          TEXT,

    -- Booking Information
    booking_id              CHAR(12),
    appointment_date        TIMESTAMPTZ,
    booking_status          TEXT,

    -- Service Details
    services                JSON,
    total_duration_minutes  INT,

    -- Financial Information
    gross_amount            NUMERIC(10,2),
    net_amount              NUMERIC(10,2),
    vat_amount              NUMERIC(10,2),
    vat_rate                NUMERIC(5,2),
    is_vat_applicable       BOOLEAN,

    -- Receipt Metadata
    receipt_generated_at    TIMESTAMPTZ,
    currency_code           TEXT
) AS $$
DECLARE
    bs RECORD;
BEGIN
    SELECT INTO bs
        bs2.business_name,
        bs2.business_address,
        bs2.business_phone,
        bs2.business_email,
        bs2.vat_registration_number,
        bs2.is_vat_registered,
        bs2.currency_code
    FROM business_settings bs2
    WHERE bs2.id = 1;

    RETURN QUERY
    SELECT
        bs.business_name,
        bs.business_address,
        bs.business_phone,
        bs.business_email,
        bs.vat_registration_number,
        bs.is_vat_registered,

        p.id AS payment_id,
        COALESCE('INV-' || p.invoice_number::text, 'INV-' || p.id) AS invoice_number,
        p.created_at AS transaction_date,
        p.payment_method::text AS payment_method,
        p.status::text AS payment_status,

        u.id AS customer_id,
        CASE
            WHEN u.id IS NULL               THEN 'Walk-in Customer'
            WHEN u.n_first_name = 'Deleted' THEN 'Walk-in Customer'
            WHEN u.account_role = 'guest'   THEN 'Walk-in Customer'
            ELSE u.fn
        END AS customer_name,
        CASE
            WHEN u.id IS NULL               THEN NULL
            WHEN u.n_first_name = 'Deleted' THEN NULL
            WHEN u.account_role = 'guest'   THEN NULL
            ELSE u.email
        END AS customer_email,
        CASE
            WHEN u.id IS NULL               THEN NULL
            WHEN u.n_first_name = 'Deleted' THEN NULL
            WHEN u.account_role = 'guest'   THEN NULL
            ELSE u.phone
        END AS customer_phone,

        b.id AS booking_id,
        b.start_time AS appointment_date,
        b.status::text AS booking_status,

        COALESCE((
            SELECT json_agg(service_obj) FROM (
                SELECT json_build_object(
                    'service_id',       s.id,
                    'name',             s.name,
                    'description',      s.description,
                    'price',            COALESCE(bsvc.override_price, s.price),
                    'duration_minutes', COALESCE(bsvc.override_duration_minutes, s.duration_minutes)
                ) AS service_obj
                FROM booking_services bsvc
                JOIN services s ON bsvc.service_id = s.id
                WHERE bsvc.booking_id = b.id
                UNION ALL
                SELECT json_build_object(
                    'service_id',       cs.id,
                    'name',             cs.name,
                    'description',      cs.description,
                    'price',            COALESCE(bcs.override_price, cs.price),
                    'duration_minutes', COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
                )
                FROM booking_custom_services bcs
                JOIN custom_services cs ON bcs.custom_service_id = cs.id
                WHERE bcs.booking_id = b.id
            ) svc_sub
        ), '[]'::json) AS services,

        COALESCE((
            SELECT SUM(dur) FROM (
                SELECT COALESCE(bsvc.override_duration_minutes, s.duration_minutes) AS dur
                FROM booking_services bsvc
                JOIN services s ON bsvc.service_id = s.id
                WHERE bsvc.booking_id = b.id
                UNION ALL
                SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
                FROM booking_custom_services bcs
                JOIN custom_services cs ON bcs.custom_service_id = cs.id
                WHERE bcs.booking_id = b.id
            ) dur_sub
        ), 0)::INT AS total_duration_minutes,

        p.amount AS gross_amount,
        COALESCE(p.net_amount, p.amount) AS net_amount,
        COALESCE(p.vat_amount, 0.00) AS vat_amount,
        p.vat_rate AS vat_rate,
        p.is_vat_applicable AS is_vat_applicable,

        NOW() AS receipt_generated_at,
        bs.currency_code AS currency_code

    FROM payments p
    JOIN bookings b ON p.booking_id = b.id
    LEFT JOIN users u ON b.user_id = u.id
    WHERE p.id = p_payment_id;
END;
$$ LANGUAGE plpgsql;

-- =======================================
-- USER SAVED CARDS TABLE (Square Integration)
-- =======================================

CREATE TABLE user_saved_cards (
    id CHAR(12) PRIMARY KEY DEFAULT generate_user_saved_card_id(),
    user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    square_card_id TEXT NOT NULL UNIQUE,
    brand TEXT NOT NULL,
    last_4 TEXT NOT NULL,
    exp_month INT NOT NULL,
    exp_year INT NOT NULL,
    fingerprint TEXT,
    is_default BOOLEAN NOT NULL DEFAULT FALSE,
    deleted_at TIMESTAMPTZ,
    deleted_by CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    retained_until TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_user_saved_cards_user ON user_saved_cards(user_id);
CREATE INDEX idx_user_saved_cards_fingerprint ON user_saved_cards(fingerprint);
CREATE INDEX idx_user_saved_cards_active ON user_saved_cards(user_id, deleted_at) WHERE deleted_at IS NULL;

-- =======================================
-- REFUNDS TABLE
-- =======================================

CREATE TABLE refunds (
    id CHAR(12) PRIMARY KEY DEFAULT generate_refund_id(),
    payment_id CHAR(12) NOT NULL REFERENCES payments(id) ON DELETE CASCADE,
    booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
    amount NUMERIC(10,2) NOT NULL CHECK (amount > 0),
    square_refund_id TEXT,
    status payment_status NOT NULL DEFAULT 'pending',
    reason TEXT NOT NULL,
    created_by CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_refunds_payment ON refunds(payment_id);
CREATE INDEX idx_refunds_booking ON refunds(booking_id);

-- =======================================
-- TILL SALES TABLE
-- Point-of-sale transactions not linked to a booking (gift card purchases, retail products, etc.)
-- Extensible for future at-the-till products via till_item_type + item_id
-- =======================================

CREATE TABLE till_sales (
    id CHAR(12) PRIMARY KEY DEFAULT generate_till_sale_id(),
    item_type till_item_type NOT NULL,
    item_id CHAR(12),                          -- FK to gift_cards, future product tables
    description TEXT NOT NULL DEFAULT '',
    quantity INT NOT NULL DEFAULT 1,
    unit_price NUMERIC(10,2) NOT NULL,
    total_amount NUMERIC(10,2) NOT NULL,
    payment_method payment_method NOT NULL,
    status payment_status NOT NULL DEFAULT 'pending',
    user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,       -- customer (nullable for walk-ins)
    user_saved_card_id CHAR(12) REFERENCES user_saved_cards(id) ON DELETE SET NULL,
    square_payment_id TEXT,
    square_checkout_id TEXT,
    idempotency_key VARCHAR(64) UNIQUE,
    notes TEXT,
    created_by CHAR(12) NOT NULL REFERENCES users(id) ON DELETE SET NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    -- VAT columns (NULL until VAT registered — SPV treatment for gift cards)
    is_vat_applicable BOOLEAN NOT NULL DEFAULT FALSE,
    vat_rate NUMERIC(5,2),
    vat_amount NUMERIC(10,2),
    net_amount NUMERIC(10,2)
);

CREATE INDEX idx_till_sales_created_at ON till_sales(created_at);
CREATE INDEX idx_till_sales_item_type ON till_sales(item_type);
CREATE INDEX idx_till_sales_user ON till_sales(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX idx_till_sales_square_checkout ON till_sales(square_checkout_id) WHERE square_checkout_id IS NOT NULL;

-- =======================================
-- FINANCIAL AGGREGATES TABLE
-- Monthly aggregated financial statistics (no PII)
-- =======================================
-- FINANCIAL AGGREGATES TABLE
-- =======================================
-- Monthly aggregates replacing granular transaction records after retention expires.
--
-- Retention: MAX(p.created_at + 7 years, user_anonymized_at + 1 year)
--   - 7 years: HMRC requirement for corporation tax records (HMRC CH14600).
--     Companies Act 2006 s.388 requires 3 years for private companies, but tax
--     law extends this to 6 years. We use 7 years as a conservative buffer.
--   - 1 year after anonymization: Allows time to handle disputes/complaints
--     after account deletion. Scottish prescriptive period is 5 years, but
--     1 year post-anonymization is sufficient for most operational needs.
--
-- Populated by CleanupExpiredFinancialRecords when granular records expire.
-- This table is retained indefinitely (no deletion) as it contains no PII.
-- =======================================

CREATE TABLE financial_aggregates (
    month DATE PRIMARY KEY,
    total_payments NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_refunds NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_square_fees NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_cash NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_online NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_in_person NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_discounts NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_giftcard NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_tips NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_deposits NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_balances NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_partials NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_vat_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
    total_net_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
    booking_count INT NOT NULL DEFAULT 0,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- =======================================
-- AFFILIATE PAYOUTS TABLE
-- =======================================

CREATE TABLE affiliate_payouts (
    id CHAR(12) PRIMARY KEY DEFAULT generate_affiliate_payout_id(),
    affiliate_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    amount NUMERIC(10,2) CHECK (amount >= 0),
    status TEXT NOT NULL DEFAULT 'pending',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_affiliate_payouts_affiliate ON affiliate_payouts(affiliate_id);

-- =======================================
-- GIFT CARDS TABLE
-- =======================================
-- Gift cards are Single-Purpose Vouchers (SPVs) under UK VAT law.
-- VAT is charged at point of sale, NOT at redemption.
--
-- Expiry: 24 months rolling from last usage (industry standard per CMA guidance).
-- UK Consumer Rights Act 2015 requires expiry terms to be "fair and transparent".
-- 24 months matches premium retailers (John Lewis, M&S, Sainsbury's) and is
-- widely considered reasonable by the CMA. Under 12 months risks being challenged
-- as an unfair contract term.
--
-- Once redeemed to an account, the value becomes account balance (no expiry on
-- the balance itself, but the account can be deleted after idle time per GDPR).
-- =======================================

CREATE TABLE gift_cards (
    id CHAR(12) PRIMARY KEY DEFAULT generate_gift_cards_id(),
    total_funds_added NUMERIC(12,2) NOT NULL DEFAULT 0,
    amount_remaining NUMERIC(12,2) NOT NULL DEFAULT 0,
    created_by CHAR(12) REFERENCES users(id),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    redeemed_at TIMESTAMPTZ,
    redeemed_by CHAR(12) REFERENCES users(id),
    is_inventory BOOLEAN NOT NULL DEFAULT FALSE,
    expiry_date TIMESTAMPTZ,
    -- Rolling expiry: 24 months from last usage (redeem, topup, balance check)
    last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    -- Voucher type at time of purchase ('SPV' or 'MPV'). Read at redemption to
    -- determine whether VAT was already paid at sale (SPV) or must be applied
    -- now (MPV). NULL if the card was created before this column was added.
    voucher_type_at_purchase VARCHAR(3)
);

CREATE INDEX idx_gift_cards_last_used_at ON gift_cards(last_used_at)
    WHERE last_used_at IS NOT NULL AND redeemed_by IS NULL AND amount_remaining > 0;

-- =======================================
-- USER GIFTCARD BALANCES TABLE
-- =======================================
-- Pooled balance from redeemed gift cards. Once redeemed, the value is no longer
-- tied to a specific gift card — it becomes account credit.
--
-- Account balances do NOT expire directly. However, the account holding the balance
-- can be deleted after idle time per GDPR storage limitation (Article 5(1)(e)):
--   - No money: 2 years idle (legitimate interest in customer relationship weakens)
--   - With money: 5 years idle (Scottish prescriptive period for contract claims,
--     Prescription and Limitation (Scotland) Act 1973 s.6)
--
-- When an account with balance is deleted, the balance moves to gift_card_expired_balances
-- for recovery claims (retained indefinitely, no PII).
-- =======================================

CREATE TABLE user_giftcard_balances (
    user_id CHAR(12) PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
    balance NUMERIC(12,2) NOT NULL DEFAULT 0,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- =======================================
-- GIFT CARD TRANSACTIONS TABLE (Audit Log)
-- =======================================

CREATE TABLE gift_card_transactions (
    id CHAR(12) PRIMARY KEY DEFAULT generate_gift_card_transactions_id(),
    gift_card_id CHAR(12) NOT NULL REFERENCES gift_cards(id),
    transaction_type VARCHAR(20) NOT NULL,
    amount NUMERIC(10,2) NOT NULL,
    reference_type VARCHAR(20),
    reference_id CHAR(12),
    user_id CHAR(12) REFERENCES users(id),
    notes TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_gift_card_transactions_card ON gift_card_transactions(gift_card_id);
CREATE INDEX idx_gift_card_transactions_type ON gift_card_transactions(transaction_type);

-- =======================================
-- GIFT CARD EXPIRED BALANCES TABLE (Recovery Mechanism)
-- =======================================
-- Dormant balances from deleted accounts. Retained indefinitely to allow recovery
-- claims via account ID.
--
-- Legal basis: UK consumer protection law treats gift card balances as the customer's
-- money held by the business. When an account is deleted (GDPR compliance), the balance
-- becomes "dormant" but remains claimable. This transforms "forfeiture" into "dormancy",
-- reducing legal risk from 40-50% to 10-20% per CMA unfair terms analysis.
--
-- Analogous to Dormant Bank and Building Society Accounts Act 2008 (15-year dormancy
-- before transfer to reclaim fund). No claim deadline imposed — consumer can recover
-- at any time with valid account ID.
--
-- No PII stored: only account ID + balance amount. After account anonymization,
-- we cannot verify claims without the account ID (by design — GDPR compliance).
-- =======================================

CREATE TABLE gift_card_expired_balances (
    id CHAR(12) PRIMARY KEY DEFAULT generate_gift_card_expired_balances_id(),
    account_id CHAR(12) NOT NULL,
    original_balance NUMERIC(10,2) NOT NULL,
    expired_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    claimed_at TIMESTAMPTZ,
    claimed_by_admin CHAR(12) REFERENCES users(id),
    notes TEXT
);

CREATE INDEX idx_gift_card_expired_balances_account ON gift_card_expired_balances(account_id);
CREATE INDEX idx_gift_card_expired_balances_unclaimed ON gift_card_expired_balances(claimed_at)
    WHERE claimed_at IS NULL;
CREATE INDEX idx_gift_card_transactions_created_at ON gift_card_transactions(created_at);

-- =======================================
-- ADMIN AUDIT LOG TABLE
-- =======================================
-- Records admin actions that access or modify user financial data.
-- Legal basis: GDPR Article 30 (records of processing) and financial audit requirements.
-- =======================================

CREATE TABLE admin_audit_log (
    id CHAR(12) PRIMARY KEY DEFAULT generate_admin_audit_log_id(),
    admin_id CHAR(12) NOT NULL REFERENCES users(id),
    action_type VARCHAR(30) NOT NULL,
    target_user_id CHAR(12) REFERENCES users(id),
    target_gift_card_id CHAR(12) REFERENCES gift_cards(id),
    details JSONB,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_admin_audit_log_admin ON admin_audit_log(admin_id);
CREATE INDEX idx_admin_audit_log_target_user ON admin_audit_log(target_user_id);
CREATE INDEX idx_admin_audit_log_created_at ON admin_audit_log(created_at);

-- =======================================
-- SQUARE DEPOSITS TABLE (Bank Reconciliation)
-- =======================================

CREATE TABLE square_deposits (
    id CHAR(12) PRIMARY KEY DEFAULT generate_square_deposit_id(),
    square_deposit_id TEXT,
    amount NUMERIC(10,2) NOT NULL,
    fees_deducted NUMERIC(10,2),
    deposited_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_square_deposits_deposited ON square_deposits(deposited_at);

-- =======================================
-- CARDDAV / CALDAV TABLES (used by SabreDAV sync in Go backend)
-- =======================================

CREATE TABLE IF NOT EXISTS dav_principals (
    id SERIAL PRIMARY KEY,
    uri VARCHAR(255) NOT NULL UNIQUE,
    email VARCHAR(255),
    displayname VARCHAR(255)
);

CREATE TABLE IF NOT EXISTS dav_calendars (
    id SERIAL PRIMARY KEY,
    principaluri VARCHAR(255) NOT NULL,
    displayname VARCHAR(255),
    uri VARCHAR(255) NOT NULL,
    description TEXT,
    calendarorder INT DEFAULT 0,
    calendarcolor VARCHAR(10),
    timezone TEXT,
    components VARCHAR(255),
    transparent BOOLEAN DEFAULT FALSE,
    UNIQUE(principaluri, uri)
);

CREATE TABLE IF NOT EXISTS dav_calendarobjects (
    id SERIAL PRIMARY KEY,
    calendardata TEXT,
    uri VARCHAR(255) NOT NULL,
    calendarid INTEGER NOT NULL REFERENCES dav_calendars(id) ON DELETE CASCADE,
    lastmodified INTEGER,
    etag VARCHAR(32),
    size INTEGER,
    componenttype VARCHAR(8),
    firstoccurence INTEGER,
    lastoccurence INTEGER,
    uid VARCHAR(255),
    UNIQUE(calendarid, uri)
);

CREATE TABLE IF NOT EXISTS dav_addressbooks (
    id SERIAL PRIMARY KEY,
    principaluri VARCHAR(255) NOT NULL,
    displayname VARCHAR(255),
    uri VARCHAR(255) NOT NULL,
    description TEXT,
    synctoken INTEGER DEFAULT 1,
    UNIQUE(principaluri, uri)
);

CREATE TABLE IF NOT EXISTS dav_cards (
    id SERIAL PRIMARY KEY,
    addressbookid INTEGER NOT NULL REFERENCES dav_addressbooks(id) ON DELETE CASCADE,
    carddata TEXT,
    uri VARCHAR(255) NOT NULL,
    lastmodified INTEGER,
    etag VARCHAR(32),
    size INTEGER,
    UNIQUE(addressbookid, uri)
);

CREATE TABLE IF NOT EXISTS dav_addressbookchanges (
    id SERIAL PRIMARY KEY,
    uri VARCHAR(255) NOT NULL,
    synctoken INTEGER NOT NULL,
    addressbookid INTEGER NOT NULL REFERENCES dav_addressbooks(id) ON DELETE CASCADE,
    operation SMALLINT NOT NULL
);

CREATE TABLE IF NOT EXISTS dav_calendarchanges (
    id SERIAL PRIMARY KEY,
    uri VARCHAR(255) NOT NULL,
    synctoken INTEGER NOT NULL,
    calendarid INTEGER NOT NULL REFERENCES dav_calendars(id) ON DELETE CASCADE,
    operation SMALLINT NOT NULL
);

CREATE TABLE IF NOT EXISTS dav_users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE,
    digesta1 VARCHAR(32) NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_calendarobjects_calendarid ON dav_calendarobjects(calendarid);
CREATE INDEX IF NOT EXISTS idx_cards_addressbookid ON dav_cards(addressbookid);

-- Seed default principal, addressbook, and calendar for CardDAV/CalDAV sync
INSERT INTO dav_principals (uri, email, displayname)
SELECT 'principals/default', 'admin@example.com', 'Default User'
WHERE NOT EXISTS (SELECT 1 FROM dav_principals WHERE uri = 'principals/default');

INSERT INTO dav_addressbooks (principaluri, displayname, uri, description, synctoken)
SELECT 'principals/default', 'Contacts', 'default', 'Default address book', 1
WHERE NOT EXISTS (SELECT 1 FROM dav_addressbooks WHERE uri = 'default');

INSERT INTO dav_calendars (principaluri, displayname, uri, description, components, transparent)
SELECT 'principals/default', 'Default Calendar', 'default', 'Default calendar', 'VEVENT,VTODO', false
WHERE NOT EXISTS (SELECT 1 FROM dav_calendars WHERE uri = 'default');

-- Account lockout audit log
CREATE TABLE IF NOT EXISTS login_audit (
    id BIGSERIAL PRIMARY KEY,
    user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
    ip_address INET NOT NULL,
    attempt_type TEXT NOT NULL, -- 'login' | 'register' | 'refresh'
    success BOOLEAN NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_login_audit_user_id ON login_audit (user_id);
CREATE INDEX IF NOT EXISTS idx_login_audit_ip ON login_audit (ip_address);
CREATE INDEX IF NOT EXISTS idx_login_audit_created_at ON login_audit (created_at);

-- =======================================
-- FUNCTION USAGE SUMMARY
-- =======================================
/*
This summary organises all database functions by purpose, legal basis, and expected
usage frequency — providing a clear guide for application integration.

--------------------------------------------------------------------------------
1. FINANCIAL & TAX COMPLIANCE
--------------------------------------------------------------------------------

REGULAR USE (called from API endpoints):
- get_sales_totals(start_date, end_date)
  → Dashboard metrics: total revenue, transaction count, payment method breakdown.
- get_receipt_data(payment_id)
  → UK-compliant receipt after every completed payment.
- calculate_vat(gross_amount, vat_rate)
  → Validate or manually compute VAT splits (only usable when VAT registered).

PERIODIC USE (monthly/quarterly for accounting or HMRC):
- export_sales_transactions(start_date, end_date, include_vat)
  → Primary export for QuickBooks / Xero. Set include_vat=true only if VAT registered.
- get_monthly_business_summary(start_date, end_date)
  → Revenue trends, avg. transaction, and payment method adoption month by month.
- get_vat_return_data(start_date, end_date)
  → MTD-ready VAT return summary for HMRC (only relevant when VAT registered).

ONE-TIME / TRANSITIONAL:
- enable_vat_registration(registration_date, vat_rate, vat_reg_number)
  → Run once on crossing the £90k VAT threshold; backfills historical payments.
- apply_vat_to_payment(payment_id, vat_rate)
  → Called for every new payment after VAT registration.
- apply_vat_to_till_sale(sale_id, vat_rate)
  → Called for every new till sale after VAT registration (gift card SPV treatment).

--------------------------------------------------------------------------------
2. GDPR & DATA PRIVACY
--------------------------------------------------------------------------------

ON-DEMAND (triggered by user request or admin action):
- export_all_user_data(user_id)
  → Full Subject Access Request (SAR) export in JSON — GDPR Article 15.
  → Includes profile, bookings (with service overrides), payments, patch tests.
- anonymize_user(user_id)
  → Right to Erasure for registered users — GDPR Article 17.
  → Preserves booking/payment audit trail; replaces PII with placeholders.
  → NOTE: phone and date_of_birth use placeholder values (NOT NULL columns).
- delete_guest_user(user_id)
  → Complete deletion of guest accounts (no contractual retention basis).
- update_data_consent(user_id, consent)
  → Records updated consent with timestamp for auditability.

NOTE: Booking/payment data is retained under contractual necessity (GDPR Art. 6(1)(b))
even after consent withdrawal. Only optional/marketing data is consent-governed.

--------------------------------------------------------------------------------
3. INTEGRATION QUICK REFERENCE
--------------------------------------------------------------------------------
• User deletes account  → anonymize_user()       or delete_guest_user()
• "What data do you have?" → export_all_user_data()
• New payment           → apply_vat_to_payment() (if VAT reg) + get_receipt_data()
• Monthly close         → export_sales_transactions(..., include_vat := [true/false])
• VAT registration day  → enable_vat_registration() once, then apply_vat_to_payment()
• Dashboard load        → get_sales_totals() + get_monthly_business_summary()
*/

-- ============================================================================
-- Auth tables
-- ============================================================================

-- Revoked JTIs for JWT token revocation
CREATE TABLE IF NOT EXISTS revoked_jtis (
    jti TEXT PRIMARY KEY,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at TIMESTAMPTZ NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_revoked_jtis_expires_at ON revoked_jtis (expires_at);

-- Refresh tokens with rotation tracking
CREATE TABLE IF NOT EXISTS refresh_tokens (
    id BIGSERIAL PRIMARY KEY,
    user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token_hash TEXT NOT NULL UNIQUE,
    role TEXT NOT NULL,
    revoked BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at TIMESTAMPTZ NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens (user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens (expires_at);

-- Migration: add voucher_type_at_purchase to gift_cards (for SPV/MPV per-card tracking)
ALTER TABLE gift_cards ADD COLUMN IF NOT EXISTS voucher_type_at_purchase VARCHAR(3);
