from datetime import date, timedelta
 
from typing import List, Tuple, Optional
 
import calendar
 
  
 
# Define allowed string options for type hinting and validation
 
PROPERTY_TYPES = ["room shared facilities", "room with private facilities", "flat", "house", "villa"]
 
AIRBNB_RANKINGS = ["super host", "guest's favourite", "other"]
 
PREDEFINED_LOCATIONS = ["Ronco", "Busalla", "Crocefieschi", "Isola del Cantone", "Minceto", "Parco dell'Antola", "Borgo Fornari", "San Bartolomeo"]
 
  
 
class BnB:
 
DEFAULT_REMOTENESS_SCORES = {
 
"Busalla": 2,
 
"Ronco": 3,
 
"Isola del Cantone": 3,
 
"Crocefieschi": 4,
 
"Minceto": 5,
 
"Parco dell'Antola": 5,
 
"Borgo Fornari": 3,
 
"San Bartolomeo": 3
 
}
 
  
 
def __init__(
 
self,
 
raw_price_config: List[Optional[int]],
 
max_total_guests: int,
 
open_date_span: Tuple[date, date],
 
unavailable_periods: List[Tuple[date, date]],
 
rented_dates: List[Tuple[date, date, int]],
 
stars: float,
 
num_reviews: int,
 
property_type: str,
 
kitchen_available: bool,
 
pets_allowed: bool = True,
 
hosting_years: int = 0,
 
airbnb_ranking: str = "other",
 
bed_distribution: str = "",
 
breakfast_offered: bool = False,
 
breakfast_quality: Optional[int] = None,
 
location_name: str = "Unknown",
 
remoteness_score_input: Optional[int] = None,
 
link = None
 
):
 
# Validations for max guests
 
if not (0 <= max_total_guests):
 
raise ValueError("Maximum total guests cannot be negative.")
 
self.max_total_guests = max_total_guests
 
  
 
# Price List Validation
 
if max_total_guests > 0 and (not raw_price_config or raw_price_config[0] is None):
 
raise ValueError("Price for at least one person must be provided in raw_price_config if max_total_guests > 0.")
 
  
 
# Process pricing logic
 
prices_for_1_to_8_people = []
 
current_price_tier = raw_price_config[0] if raw_price_config and raw_price_config[0] is not None else 0
 
for i in range(8):
 
if i < len(raw_price_config) and raw_price_config[i] is not None:
 
current_price_tier = raw_price_config[i]
 
prices_for_1_to_8_people.append(current_price_tier)
 
  
 
self.effective_nightly_prices: List[int] = []
 
if self.max_total_guests > 0:
 
for i in range(self.max_total_guests):
 
if i < 8:
 
self.effective_nightly_prices.append(prices_for_1_to_8_people[i])
 
else:
 
self.effective_nightly_prices.append(prices_for_1_to_8_people[7])
 
  
 
# Calendar Details
 
if not (isinstance(open_date_span, tuple) and len(open_date_span) == 2 and
 
isinstance(open_date_span[0], date) and isinstance(open_date_span[1], date) and
 
open_date_span[0] <= open_date_span[1]):
 
raise ValueError("open_date_span must be a tuple of two valid dates (start, end) with start <= end.")
 
self.open_date_span: Tuple[date, date] = open_date_span
 
  
 
# Unavailable Periods
 
self.unavailable_periods: List[Tuple[date, date]] = [
 
period for period in unavailable_periods if period[0] <= period[1]
 
]
 
  
 
# Rented Dates
 
self.rented_dates: List[Tuple[date, date, int]] = [
 
booking for booking in rented_dates if booking[0] <= booking[1] and booking[2] > 0
 
]
 
  
 
# Validate stars
 
if not (0.0 <= stars <= 5.0):
 
raise ValueError("Stars must be a float between 0.0 and 5.0.")
 
self.stars: float = stars
 
  
 
# Validate number of reviews
 
if not isinstance(num_reviews, int) or num_reviews < 0:
 
raise ValueError("Number of reviews must be a non-negative integer.")
 
self.num_reviews: int = num_reviews
 
  
 
# Validate property type
 
if property_type not in PROPERTY_TYPES:
 
raise ValueError(f"Invalid property_type: '{property_type}'. Must be one of {PROPERTY_TYPES}")
 
self.property_type: str = property_type
 
  
 
self.kitchen_available: bool = kitchen_available
 
self.pets_allowed: bool = pets_allowed
 
self.hosting_years: int = max(hosting_years, 0)
 
self.airbnb_ranking: str = airbnb_ranking if airbnb_ranking in AIRBNB_RANKINGS else "other"
 
self.bed_distribution: str = bed_distribution
 
self.breakfast_offered: bool = breakfast_offered
 
  
 
if self.breakfast_offered:
 
if breakfast_quality is None or not (0 <= breakfast_quality <= 5):
 
raise ValueError("Breakfast quality must be between 0 and 5 if breakfast is offered.")
 
self.breakfast_quality: Optional[int] = breakfast_quality
 
else:
 
self.breakfast_quality: Optional[int] = None
 
  
 
self.location_name: str = location_name
 
  
 
if remoteness_score_input is not None:
 
if not (0 <= remoteness_score_input <= 5):
 
raise ValueError("Remoteness score must be between 0 and 5.")
 
self.remoteness_score: int = remoteness_score_input
 
elif location_name in self.DEFAULT_REMOTENESS_SCORES:
 
self.remoteness_score: int = self.DEFAULT_REMOTENESS_SCORES[location_name]
 
else:
 
raise ValueError(f"Remoteness score must be provided for unrecognized location: '{location_name}'.")
 
  
 
def get_price_for_guests(self, num_guests: int) -> Optional[int]:
 
if not (1 <= num_guests <= self.max_total_guests):
 
return None
 
return self.effective_nightly_prices[num_guests - 1]
 
  
 
def calculate_revenue(self) -> float:
 
"""Calculate total revenue based on rented dates and nightly price."""
 
revenue = 0
 
for start_date, end_date, guests in self.rented_dates:
 
nights = (end_date - start_date).days # Calculate number of nights
 
price_per_night = self.get_price_for_guests(guests)
 
if price_per_night:
 
revenue += nights * price_per_night
 
return revenue
 
  
 
def __repr__(self):
 
return (f"BnB(location='{self.location_name}', max_guests={self.max_total_guests}, "
 
f"stars={self.stars}, type='{self.property_type}', reviews={self.num_reviews})")
 
  
 
# --- Airbnb Instances ---
 
bnb_list: List[BnB] = [
 
BnB(raw_price_config=[82], max_total_guests=4, open_date_span=(date(2025, 5, 25), date(2025, 7, 23)), unavailable_periods=[], rented_dates=[
 
(date(2025, 5, 27), date(2025, 6, 1), 2),
 
(date(2025, 6, 6), date(2025, 6, 10), 2),
 
(date(2025, 6, 14), date(2025, 6, 15), 2),
 
(date(2025, 6, 19), date(2025, 6, 22), 2),
 
(date(2025, 7, 8), date(2025, 7, 15), 2)
 
], stars=4.96, num_reviews=49, property_type="flat", kitchen_available=True, pets_allowed=True, hosting_years=2, airbnb_ranking="super host", bed_distribution="2 bedrooms, 3 beds, 1 bathroom", breakfast_offered=False, location_name="Crocefieschi"),
 
BnB(raw_price_config=[69], max_total_guests=3, open_date_span=(date(2025, 5, 25), date(2025, 10, 30)), unavailable_periods=[], rented_dates=[
 
(date(2025, 5, 27), date(2025, 6, 7), 2),
 
(date(2025, 6, 16), date(2025, 6, 22), 2),
 
(date(2025, 6, 28), date(2025, 7, 8), 2),
 
(date(2025, 8, 2), date(2025, 8, 15), 2),
 
(date(2025, 8, 30), date(2025, 9, 10), 2)
 
], stars=5.0, num_reviews=52, property_type="flat", kitchen_available=True, pets_allowed=True, hosting_years=2, airbnb_ranking="super host", bed_distribution="1 bedrooms, 2 beds, 1 bathroom", breakfast_offered=False, location_name="Borgo Fornari"),
 
BnB(raw_price_config=[64], max_total_guests=5, open_date_span=(date(2025, 6, 1), date(2025, 9, 30)), unavailable_periods=[], rented_dates=[
 
(date(2025, 6, 6), date(2025, 6, 7), 2),
 
(date(2025, 6, 10), date(2025, 6, 15), 2),
 
(date(2025, 6, 21), date(2025, 7, 22), 2),
 
(date(2025, 7, 2), date(2025, 7, 2), 2),
 
(date(2025, 8, 8), date(2025, 8, 10), 2),
 
(date(2025, 8, 17), date(2025, 8, 18), 2),
 
(date(2025, 9, 3), date(2025, 9, 11), 2),
 
(date(2025, 9, 27), date(2025, 9, 30), 2)
 
], stars=5.0, num_reviews=52, property_type="room with private facilities", kitchen_available=True, pets_allowed=True, hosting_years=2, airbnb_ranking="super host", bed_distribution="B&B", breakfast_offered=True, breakfast_quality=3, location_name="Busalla"),
 
BnB(raw_price_config=[60], max_total_guests=4, open_date_span=(date(2025, 5, 25), date(2025, 10, 1)), unavailable_periods=[], rented_dates=[
 
(date(2025, 5, 30), date(2025, 7, 2), 2),
 
(date(2025, 6, 7), date(2025, 6, 9), 2),
 
(date(2025, 6, 20), date(2025, 7, 22), 2),
 
(date(2025, 8, 3), date(2025, 8, 5), 2),
 
(date(2025, 8, 8), date(2025, 8, 10), 2),
 
(date(2025, 8, 16), date(2025, 8, 23), 2),
 
(date(2025, 8, 29), date(2025, 9, 12), 2)
 
], stars=4.87, num_reviews=117, property_type="flat", kitchen_available=True, pets_allowed=True, hosting_years=7, airbnb_ranking="none", bed_distribution="1 bedrooms, 2 beds, 1 bathroom", breakfast_offered=False, location_name="San Bartolomeo")
 
]
 
# --- Exclude BnBs based on User Choice ---
 
excluded_indices = {} # Example: Exclude the 3rd Airbnb in the list
 
bnb_list_filtered = [bnb for idx, bnb in enumerate(bnb_list) if (idx + 1) not in excluded_indices]
 
  
 
print("List of Considered BnBs:")
 
for idx, bnb in enumerate(bnb_list_filtered):
 
print(f" {idx + 1}: {bnb.location_name} ({bnb.property_type}) - Stars: {bnb.stars}, Reviews: {bnb.num_reviews}")
 
  
 
# --- Run Calculations ---
 
  
 
# --- Configuration Parameters ---
 
income_percentage_out = 0.05 # 15% tax
 
fixed_cost_per_booking = 0 # Fixed costs per booking (set to 0 for now)
 
house_value_eur = 18000.00 # Value of the house
 
  
 
# --- Initialize Data Structures ---
 
average_gross_monthly_profit = {}
 
average_net_monthly_profit = {}
 
bnb_active_days_per_month = {}
 
net_monthly_average_per_day = {}
 
percentage_open_airbnb = {}
 
  
 
# --- Calculate Overall Date Range for All BnBs ---
 
start_dates = [bnb.open_date_span[0] for bnb in bnb_list_filtered]
 
end_dates = [bnb.open_date_span[1] for bnb in bnb_list_filtered]
 
overall_start_date = min(start_dates)
 
overall_end_date = max(end_dates)
 
  
 
# Initialize data structures for each month in the range
 
current_date_iterator = overall_start_date
 
while current_date_iterator <= overall_end_date:
 
year_month = (current_date_iterator.year, current_date_iterator.month)
 
average_gross_monthly_profit[year_month] = 0
 
average_net_monthly_profit[year_month] = 0
 
bnb_active_days_per_month[year_month] = {}
 
net_monthly_average_per_day[year_month] = 0
 
percentage_open_airbnb[year_month] = 0
 
current_date_iterator = (current_date_iterator.replace(day=28) + timedelta(days=4)).replace(day=1)
 
  
 
total_bnb_count = len(bnb_list_filtered)
 
  
 
# --- Compute Monthly Profits and Stats ---
 
for bnb in bnb_list:
 
for booking_start_date, booking_end_date, guests in bnb.rented_dates:
 
current_night = booking_start_date
 
while current_night < booking_end_date:
 
year_month = (current_night.year, current_night.month)
 
  
 
# Gross and net profit computations
 
price_per_night = bnb.get_price_for_guests(guests) or 0
 
average_gross_monthly_profit[year_month] += price_per_night / total_bnb_count
 
average_net_monthly_profit[year_month] += (price_per_night * (1 - income_percentage_out)) / total_bnb_count
 
  
 
# Track days of activity for each BnB in this month
 
if bnb not in bnb_active_days_per_month[year_month]:
 
bnb_active_days_per_month[year_month][bnb] = set()
 
bnb_active_days_per_month[year_month][bnb].add(current_night)
 
  
 
current_night += timedelta(days=1)
 
  
 
# --- Finalize Calculations for Averages and Open Percentages ---
 
for year_month in average_gross_monthly_profit.keys():
 
# Total days in the month
 
days_in_month = calendar.monthrange(year_month[0], year_month[1])[1]
 
  
 
# Calculate the daily average net profit (net profit / total days in the month)
 
net_monthly_average_per_day[year_month] = average_net_monthly_profit[year_month] / days_in_month
 
  
 
# Determine the percentage of Airbnbs open in this month
 
bnbs_open_in_month = sum(
 
1 for bnb in bnb_list_filtered
 
if bnb.open_date_span[0] <= date(year_month[0], year_month[1], days_in_month) and
 
bnb.open_date_span[1] >= date(year_month[0], year_month[1], 1)
 
)
 
percentage_open_airbnb[year_month] = (bnbs_open_in_month / total_bnb_count) * 100
 
  
 
# --- Output Results ---
 
print("Enhanced Market Analysis:")
 
print(f" Overall Airbnb Open Dates: {overall_start_date} to {overall_end_date}\n")
 
  
 
print(" Monthly Profit Analysis (Averages Across All BnBs):")
 
seasonal_gross_revenue = {"Spring": 0, "Summer": 0, "Autumn": 0, "Winter": 0}
 
seasonal_net_revenue = {"Spring": 0, "Summer": 0, "Autumn": 0, "Winter": 0}
 
  
 
seasons_map = {
 
3: "Spring", 4: "Spring", 5: "Spring",
 
6: "Summer", 7: "Summer", 8: "Summer",
 
9: "Autumn", 10: "Autumn", 11: "Autumn",
 
12: "Winter", 1: "Winter", 2: "Winter",
 
}
 
  
 
total_months_reported = 0
 
total_avg_gross_revenue = 0
 
total_avg_net_revenue = 0
 
  
 
for (year, month), gross in sorted(average_gross_monthly_profit.items()):
 
if gross > 0:
 
net = average_net_monthly_profit[(year, month)]
 
net_avg_per_day = net_monthly_average_per_day[(year, month)]
 
airbnb_open_percentage = percentage_open_airbnb[(year, month)]
 
month_name = calendar.month_name[month]
 
  
 
print(f"{month_name} {year}")
 
print(f" Average Gross Profit Per BnB: €{gross:.2f}")
 
print(f" Average Net Profit Per BnB: €{net:.2f}")
 
print(f" Average Net Profit Per Day: €{net_avg_per_day:.2f}")
 
print(f" Percentage of Airbnbs Open: {airbnb_open_percentage:.2f}%\n")
 
  
 
# Update seasonal revenues (normalized for average per Airbnb)
 
seasonal_gross_revenue[seasons_map[month]] += gross
 
seasonal_net_revenue[seasons_map[month]] += net
 
  
 
total_months_reported += 1
 
total_avg_gross_revenue += gross
 
total_avg_net_revenue += net
 
  
 
# --- Display Seasonal and Overall Totals ---
 
print("\nSeasonal Net Revenue (Per Average Airbnb):")
 
for season, revenue in seasonal_net_revenue.items():
 
print(f" {season}: €{revenue:.2f}")
 
  
 
if total_months_reported > 0:
 
print(f"\nAverage Yearly Gross Revenue (Per Airbnb): €{total_avg_gross_revenue:.2f}")
 
print(f"Average Yearlt Net Revenue (Per Airbnb): €{total_avg_net_revenue:.2f}")
 
  
 
if house_value_eur > 0:
 
net_revenue_ratio = (total_avg_net_revenue / house_value_eur) * 100
 
print(f"\nNet Revenue as % of House Value: {net_revenue_ratio:.2f}%")
 
else:
 
print("\nHouse value not set or zero, cannot calculate net revenue as percentage of house value.")