/*
 * Expense Policy Rule Engine
 * 
 * Problem asked in a Rippling interview.
 * 
 * You get a list of expenses belonging to trips. Evaluate them against 
 * company policies and flag violations with reasons.
 * 
 * Rules exist at two levels:
 * - Expense-level (per expense)
 * - Trip-level (across expenses of a trip)
 * 
 * Design approach (matching the folder structure):
 * - model/          → Expense, ExpenseType, Trip, Violation
 * - rules/          → ExpenseRule, TripRule interfaces
 * - rules/impl/     → DisallowRule, MaxAmountRule, TripTotalMaxRule, TripDurationRule
 * - registry/       → RuleRegistry
 * - engine/         → RuleEngine
 * - service/        → ExpensePolicyService (facade)
 * - util/           → TripValidator
 * 
 * New rules can be added by implementing the right interface and registering them.
 * No existing code needs to change.
 * 
 * Note: Everything is in one file so it runs on Ideone.
 * In a real project this would follow the folder structure shown in the diagram.
 */

import java.util.*;
import java.util.stream.Collectors;

enum ExpenseType {
    RESTAURANT, AIRFARE, ENTERTAINMENT, HOTEL, SUPPLIES
}

// ==================== model/ ====================
// Represents a single expense submitted by an employee
class Expense {
    private final String expenseId;
    private final String tripId;
    private final double amountUSD;
    private final ExpenseType type;

    public Expense(String expenseId, String tripId, double amountUSD, ExpenseType type) {
        this.expenseId = expenseId;
        this.tripId = tripId;
        this.amountUSD = amountUSD;
        this.type = type;
    }

    public String getExpenseId() { return expenseId; }
    public String getTripId() { return tripId; }
    public double getAmountUSD() { return amountUSD; }
    public ExpenseType getType() { return type; }
}

// Represents a trip that groups multiple expenses
class Trip {
    private final String tripId;
    private final String name;
    private final int durationDays;

    public Trip(String tripId, String name, int durationDays) {
        this.tripId = tripId;
        this.name = name;
        this.durationDays = durationDays;
    }

    public String getTripId() { return tripId; }
    public String getName() { return name; }
    public int getDurationDays() { return durationDays; }
}

// Holds details of a rule violation
class Violation {
    private final String message;
    private final String expenseId; // null for trip-level violations

    private Violation(String message, String expenseId) {
        this.message = message;
        this.expenseId = expenseId;
    }

    public static Violation of(String message) {
        return new Violation(message, null);
    }

    public static Violation of(String message, String expenseId) {
        return new Violation(message, expenseId);
    }

    public String getMessage() { return message; }
    public String getExpenseId() { return expenseId; }

    @Override
    public String toString() {
        return expenseId != null ? "[" + expenseId + "] " + message : message;
    }
}

// ==================== rules/ ====================
// Contract for rules that check a single expense
interface ExpenseRule {
    Optional<Violation> check(Expense expense);
}

// Contract for rules that check an entire trip
interface TripRule {
    Optional<Violation> check(Trip trip, List<Expense> expenses);
}

// ==================== rules/impl/ ====================
// Prevents a specific expense type (e.g. no airfare allowed)
class DisallowRule implements ExpenseRule {
    private final ExpenseType disallowedType;

    public DisallowRule(ExpenseType disallowedType) {
        this.disallowedType = disallowedType;
    }

    @Override
    public Optional<Violation> check(Expense expense) {
        if (expense.getType() == disallowedType) {
            return Optional.of(Violation.of(disallowedType + " expenses are not allowed", expense.getExpenseId()));
        }
        return Optional.empty();
    }
}

// Checks if a single expense exceeds a maximum amount
class MaxAmountRule implements ExpenseRule {
    private final double maxAmount;

    public MaxAmountRule(double maxAmount) {
        this.maxAmount = maxAmount;
    }

    @Override
    public Optional<Violation> check(Expense expense) {
        if (expense.getAmountUSD() > maxAmount) {
            return Optional.of(Violation.of(
                "Amount $" + expense.getAmountUSD() + " exceeds max $" + maxAmount,
                expense.getExpenseId()));
        }
        return Optional.empty();
    }
}

// Checks if the total spend of a trip exceeds a limit
class TripTotalMaxRule implements TripRule {
    private final double maxAmount;

    public TripTotalMaxRule(double maxAmount) {
        this.maxAmount = maxAmount;
    }

    @Override
    public Optional<Violation> check(Trip trip, List<Expense> expenses) {
        double total = expenses.stream().mapToDouble(Expense::getAmountUSD).sum();
        if (total > maxAmount) {
            return Optional.of(Violation.of("Trip total $" + total + " exceeds max $" + maxAmount));
        }
        return Optional.empty();
    }
}

// Checks if the trip duration exceeds a limit
class TripDurationRule implements TripRule {
    private final int maxDays;

    public TripDurationRule(int maxDays) {
        this.maxDays = maxDays;
    }

    @Override
    public Optional<Violation> check(Trip trip, List<Expense> expenses) {
        if (trip.getDurationDays() > maxDays) {
            return Optional.of(Violation.of("Trip duration " + trip.getDurationDays() + " days exceeds max " + maxDays));
        }
        return Optional.empty();
    }
}

// ==================== util/ ====================
// Optional helper to validate that all expenses belong to the same trip
class TripValidator {
    public static boolean allExpensesBelongToSameTrip(List<Expense> expenses) {
        if (expenses == null || expenses.isEmpty()) return true;
        String tripId = expenses.get(0).getTripId();
        return expenses.stream().allMatch(e -> e.getTripId().equals(tripId));
    }
}

// ==================== registry/ ====================
// Maintains which rules apply at expense and trip levels
class RuleRegistry {
    private final Map<ExpenseType, List<ExpenseRule>> typeRules = new HashMap<>();
    private final List<ExpenseRule> globalExpenseRules = new ArrayList<>();
    private final List<TripRule> tripRules = new ArrayList<>();

    public void addTypeRule(ExpenseType type, ExpenseRule rule) {
        typeRules.computeIfAbsent(type, k -> new ArrayList<>()).add(rule);
    }

    public void addGlobalExpenseRule(ExpenseRule rule) {
        globalExpenseRules.add(rule);
    }

    public void addTripRule(TripRule rule) {
        tripRules.add(rule);
    }

    // Returns type-specific + global expense rules
    public List<ExpenseRule> getRulesFor(ExpenseType type) {
        List<ExpenseRule> rules = new ArrayList<>(globalExpenseRules);
        rules.addAll(typeRules.getOrDefault(type, Collections.emptyList()));
        return rules;
    }

    public List<TripRule> getTripRules() {
        return tripRules;
    }
}

// ==================== engine/ ====================
interface RuleEngine {
    List<Violation> evaluate(Trip trip, List<Expense> expenses, RuleRegistry registry);
}

class SimpleRuleEngine implements RuleEngine {
    @Override
    public List<Violation> evaluate(Trip trip, List<Expense> expenses, RuleRegistry registry) {
        List<Violation> violations = new ArrayList<>();

        // Optional validation
        if (!TripValidator.allExpensesBelongToSameTrip(expenses)) {
            violations.add(Violation.of("Expenses belong to different trips"));
            return violations;
        }

        // Apply all expense-level rules
        for (Expense expense : expenses) {
            for (ExpenseRule rule : registry.getRulesFor(expense.getType())) {
                rule.check(expense).ifPresent(violations::add);
            }
        }

        // Apply all trip-level rules
        for (TripRule rule : registry.getTripRules()) {
            rule.check(trip, expenses).ifPresent(violations::add);
        }

        return violations;
    }
}

// ==================== service/ ====================
// Facade / entry point that clients use to evaluate expenses
class ExpensePolicyService {
    private final RuleRegistry registry;
    private final RuleEngine engine;

    public ExpensePolicyService(RuleRegistry registry, RuleEngine engine) {
        this.registry = registry;
        this.engine = engine;
    }

    public List<Violation> evaluate(Trip trip, List<Expense> expenses) {
        return engine.evaluate(trip, expenses, registry);
    }
}

// ==================== Main (for Ideone) ====================
public class Main {
    public static void main(String[] args) {
        // Sample trip and expenses
        Trip trip = new Trip("T1", "Client Visit", 5);

        List<Expense> expenses = Arrays.asList(
            new Expense("E1", "T1", 80, ExpenseType.RESTAURANT),
            new Expense("E2", "T1", 120, ExpenseType.AIRFARE),
            new Expense("E3", "T1", 40, ExpenseType.ENTERTAINMENT),
            new Expense("E4", "T1", 200, ExpenseType.HOTEL)
        );

        // Register rules
        RuleRegistry registry = new RuleRegistry();
        registry.addTypeRule(ExpenseType.RESTAURANT, new MaxAmountRule(75));
        registry.addTypeRule(ExpenseType.AIRFARE, new DisallowRule(ExpenseType.AIRFARE));
        registry.addTypeRule(ExpenseType.ENTERTAINMENT, new DisallowRule(ExpenseType.ENTERTAINMENT));
        registry.addGlobalExpenseRule(new MaxAmountRule(250));
        registry.addTripRule(new TripTotalMaxRule(300));
        registry.addTripRule(new TripDurationRule(7));

        // Evaluate using the service
        ExpensePolicyService service = new ExpensePolicyService(registry, new SimpleRuleEngine());
        List<Violation> violations = service.evaluate(trip, expenses);

        System.out.println("=== Violations ===");
        if (violations.isEmpty()) {
            System.out.println("No violations.");
        } else {
            violations.forEach(v -> System.out.println("- " + v));
        }
    }
}