#!/usr/bin/env python3
"""Single source of truth for the sample property set.

Imported by make-pages.py (search grid, facets) and make-listings.py (one detail
page per property), so the grid and the detail pages can never disagree.

All values are SAMPLE DATA. Secondary details (lot size, year built, taxes, bids,
schools) are DERIVED deterministically from the primary fields rather than typed
by hand — that keeps regeneration reproducible and makes it obvious at a glance
that nothing here is a real property record.
"""

# addr, zip, price, beds, baths, sqft, status, state, type, hood, days, views
LISTINGS = [
    ("1214 Rothsay Cir", "23220", 425000, 4, 3, 3450, "For Sale", "active", "Single family", "The Fan", 21, 1254),
    ("908 Hanover Ave", "23220", 389500, 3, 2, 2180, "For Sale", "active", "Single family", "The Fan", 14, 982),
    ("4417 Kensington Ave", "23221", 512000, 4, 3, 2940, "Under Contract", "pending", "Single family", "Museum District", 37, 640),
    ("2201 E Broad St", "23223", 298000, 2, 2, 1510, "For Sale", "active", "Townhouse", "Church Hill", 9, 415),
    ("715 N 25th St", "23223", 264900, 3, 1, 1340, "For Sale", "active", "Townhouse", "Church Hill", 46, 388),
    ("3300 Monument Ave", "23221", 749000, 5, 4, 4120, "For Sale", "active", "Single family", "Museum District", 5, 1602),
    ("1801 Grove Ave", "23220", 615000, 4, 3, 3180, "For Sale", "active", "Single family", "The Fan", 12, 733),
    ("506 N 32nd St", "23223", 232500, 2, 1, 1120, "For Sale", "active", "Townhouse", "Church Hill", 63, 271),
    ("4120 Grove Ave", "23221", 468000, 3, 2, 2260, "Under Contract", "pending", "Single family", "Museum District", 28, 522),
    ("11 S Belmont Ave", "23221", 342000, 3, 2, 1780, "For Sale", "active", "Townhouse", "Museum District", 17, 449),
    ("2809 Hanes Ave", "23222", 289000, 3, 2, 1650, "For Sale", "active", "Single family", "Ginter Park", 33, 356),
    ("1310 Bellevue Ave", "23227", 379900, 4, 2, 2410, "For Sale", "active", "Single family", "Ginter Park", 7, 588),
    ("5501 Riverside Dr", "23225", 825000, 5, 4, 4680, "For Sale", "active", "Single family", "Westover Hills", 3, 1911),
    ("1204 Westover Hills Blvd", "23225", 455000, 4, 3, 2870, "For Sale", "active", "Single family", "Westover Hills", 24, 604),
    ("3010 W Clay St", "23230", 318000, 2, 2, 1440, "For Sale", "active", "Condo", "Scott's Addition", 11, 497),
    ("1400 Summit Ave", "23230", 274500, 1, 1, 980, "Sold", "closed", "Condo", "Scott's Addition", 88, 1043),
    ("13905 Midlothian Tpke", "23113", 549000, 4, 3, 3320, "For Sale", "active", "Single family", "Midlothian", 19, 712),
    ("2416 Winterfield Rd", "23113", 692000, 5, 4, 3950, "For Sale", "active", "Single family", "Midlothian", 41, 866),
]

# --- Map coordinates -------------------------------------------------------
# Approximate centroids of each Richmond neighborhood (WGS84). Pins are placed
# at the centroid plus a small DETERMINISTIC offset derived from the address,
# never by geocoding the street address itself.
#
# That is deliberate. These are SAMPLE listings with invented prices; geocoding
# the real addresses would drop fake asking prices onto real people's houses in
# a client-facing demo. Centroid + offset demonstrates the map-panel component
# just as well and implicates no real property.
#
# In production, Rule 3 puts real coordinates on the Search API index (geofield,
# populated by Nominatim at save time) and the map reads them from there.
HOOD_CENTROIDS = {
    "The Fan":          (37.5515, -77.4585),
    "Church Hill":      (37.5305, -77.4195),
    "Museum District":  (37.5565, -77.4755),
    "Ginter Park":      (37.5880, -77.4405),
    "Westover Hills":   (37.5145, -77.4835),
    "Scott's Addition": (37.5625, -77.4795),
    "Midlothian":       (37.5060, -77.6480),
}


def coords(addr, hood):
    """Stable pseudo-random offset from the neighborhood centroid.

    Derived from the address string, so a given listing always lands on the same
    spot and regenerating the prototype never moves the pins.
    """
    lat, lng = HOOD_CENTROIDS[hood]
    h = 0
    for ch in addr:
        h = (h * 31 + ord(ch)) & 0xFFFFFFFF
    # +/- ~0.012 deg lat (~1.3 km), +/- ~0.015 deg lng — enough to spread pins
    # inside a neighborhood without pushing them into the next one.
    dlat = ((h & 0xFFF) / 4095.0 - 0.5) * 0.024
    dlng = (((h >> 12) & 0xFFF) / 4095.0 - 0.5) * 0.030
    return round(lat + dlat, 6), round(lng + dlng, 6)


NEIGHBORHOODS = ["The Fan", "Church Hill", "Museum District", "Ginter Park",
                 "Westover Hills", "Scott's Addition", "Midlothian"]
TYPES = ["Single family", "Townhouse", "Condo"]
PRICE_BANDS = [("Under $250,000", 0, 249999), ("$250,000–$400,000", 250000, 399999),
               ("$400,000–$600,000", 400000, 599999), ("$600,000+", 600000, 99999999)]

# Neighborhood → the schools serving it. Source for a real build is the VDOE /
# NCES public datasets (never GreatSchools — that score is their licensed product).
SCHOOLS = {
    "The Fan": [("George W. Carver Elementary", "K–5", "0.3 mi", 420),
                ("Albert Hill Middle School", "6–8", "1.6 mi", 453),
                ("Open High School", "9–12", "1.4 mi", 183)],
    "Church Hill": [("Chimborazo Elementary", "K–5", "0.4 mi", 361),
                    ("Martin Luther King Jr. Middle", "6–8", "0.9 mi", 502),
                    ("Armstrong High School", "9–12", "1.8 mi", 640)],
    "Museum District": [("Mary Munford Elementary", "K–5", "0.6 mi", 512),
                        ("Albert Hill Middle School", "6–8", "0.8 mi", 453),
                        ("Thomas Jefferson High School", "9–12", "1.1 mi", 891)],
    "Ginter Park": [("Ginter Park Elementary", "K–5", "0.5 mi", 398),
                    ("Henderson Middle School", "6–8", "1.2 mi", 471),
                    ("John Marshall High School", "9–12", "1.7 mi", 733)],
    "Westover Hills": [("Westover Hills Elementary", "K–5", "0.3 mi", 344),
                       ("Lucille M. Brown Middle", "6–8", "1.4 mi", 486),
                       ("Huguenot High School", "9–12", "2.1 mi", 1024)],
    "Scott's Addition": [("Mary Munford Elementary", "K–5", "1.5 mi", 512),
                         ("Albert Hill Middle School", "6–8", "1.9 mi", 453),
                         ("Thomas Jefferson High School", "9–12", "1.2 mi", 891)],
    "Midlothian": [("Bettie Weaver Elementary", "K–5", "0.8 mi", 604),
                   ("Midlothian Middle School", "6–8", "1.6 mi", 812),
                   ("Midlothian High School", "9–12", "2.3 mi", 1487)],
}

HEATING = ["Gas forced air / central air", "Heat pump", "Gas forced air / central air",
           "Electric baseboard / window units", "Dual-zone heat pump"]


def slug(addr):
    """'1214 Rothsay Cir' -> '1214-rothsay-cir'"""
    out = "".join(c.lower() if c.isalnum() else "-" for c in addr)
    while "--" in out:
        out = out.replace("--", "-")
    return out.strip("-")


def filename(addr):
    return f"listing-{slug(addr)}.html"


def money(n):
    return "${:,}".format(n)


def details(row, i):
    """Derive the secondary facts a detail page needs.

    Deterministic functions of the primary fields — no randomness, so the pages
    regenerate identically. Everything returned is sample data.
    """
    addr, zipc, price, beds, baths, sqft, status, state, ptype, hood, days, views = row

    lot = {"Single family": 0.14 + (i % 7) * 0.06,
           "Townhouse": 0.05 + (i % 4) * 0.02,
           "Condo": 0.0}[ptype]
    year = 1908 + (i * 7) % 106          # 1908–2013
    tax = round(price * 0.0086 / 10) * 10  # Richmond ≈ $0.86 per $100 assessed
    hoa = {"Condo": 180 + (i % 5) * 25, "Townhouse": 60 + (i % 4) * 15,
           "Single family": 0}[ptype]
    roof = 1 + (i * 3) % 20
    saves = max(3, views // 90)

    # Bids: more views and more days on market produce more bids. The top bid
    # sits just under asking; each earlier bid steps further down.
    n_bids = 0 if state == "closed" else min(6, views // 260 + days // 30)
    bids = []
    for b in range(n_bids):
        amount = int(price * (0.97 - b * 0.055) / 50) * 50
        bids.append((f"#26{(i * 7919 + b * 104729) % 10000000000:010d}",
                     f"{(b * 5 + 2) % 12 + 1}/{(b * 7 + 3) % 28 + 1}/26",
                     amount))

    return {
        "lot": f"{lot:.2f} acres" if lot else "—",
        "year": year, "tax": tax, "hoa": hoa, "roof": roof, "saves": saves,
        "heating": HEATING[i % len(HEATING)],
        "schools": SCHOOLS[hood],
        "bids": bids,
        "ppsf": round(price / sqft),
    }


# --- systems, condition and running costs -----------------------------------
# Build manual 5a-5c model a listing far past the nine facts the detail page
# used to print: plumbing, electrical, foundation, structure, and the monthly
# cost of actually living there. Buyers compare on exactly these (13-SDC:
# compare-table takes "ALL systems detail"), so they belong on the page.
#
# Every value below is DERIVED from the primary fields the same way lot size and
# roof age already were — deterministic, reproducible, and obviously sample
# data. Nothing here is a claim about a real property. The machine name on each
# row is the manual field the Drupal build reads it from.

_WINDOWS = ["double_hung", "casement", "sliding", "double_hung", "awning"]
_COUNTER = ["granite", "quartz", "laminate", "quartz", "marble", "tile"]
_SUPPLY = ["copper", "pex", "copper", "cpvc", "pex", "galvanized"]
_WIRING = ["copper", "copper", "copper", "aluminum", "copper", "knob_tube"]
_PANEL = ["200a", "150a", "100a", "200a", "100a", "60a"]
_STRUCTURE = ["brick_veneer", "wood_frame", "brick_veneer", "stucco", "wood_frame"]
_SEWER = {"Single family": "public", "Townhouse": "public", "Condo": "public"}


def _spoken(token):
    """Machine value -> label. Kept in step with make-listing-wizard.spoken()."""
    special = {"a_ae": "Zone A / AE", "x_shaded": "Zone X (shaded)",
               "x_unshaded": "Zone X (minimal risk)", "knob_tube": "Knob and tube",
               "pier_beam": "Pier and beam", "pex": "PEX", "cpvc": "CPVC",
               "double_hung": "Double-hung", "brick_veneer": "Brick veneer",
               "wood_frame": "Wood frame", "crawl_space": "Crawl space"}
    if token in special:
        return special[token]
    if token.endswith("a") and token[:-1].isdigit():
        return token[:-1] + " amp service"
    return token.replace("_", " ").capitalize()


def systems(row, i):
    """[(section, [(label, value, machine name)])] — the full detail contract."""
    addr, zipc, price, beds, baths, sqft, status, state, ptype, hood, days, views = row
    d = details(row, i)

    full = int(baths)
    half = 1 if baths != full else 0
    levels = 1 if ptype == "Condo" else (2 if sqft < 3000 else 3)
    garage = ("none" if ptype == "Condo"
              else ("detached" if i % 3 == 0 else "attached"))
    garage_spaces = 0 if garage == "none" else 1 + i % 2
    foundation = ("slab" if ptype == "Condo"
                  else ("basement" if i % 3 == 1 else "crawl_space"))
    # Utilities scale with conditioned floor area; insurance with value.
    electric = round(sqft * 0.045 / 5) * 5
    gas = round(sqft * 0.021 / 5) * 5
    water = 40 + (i % 5) * 6
    trash = 22 + (i % 3) * 4
    insurance = round(price * 0.0042 / 10) * 10
    walk = {"The Fan": 92, "Church Hill": 84, "Museum District": 88,
            "Ginter Park": 71, "Westover Hills": 63, "Scott's Addition": 90,
            "Midlothian": 38}[hood]

    return [
        ("Size and layout", [
            ("Bedrooms", beds, "field_prop_beds"),
            ("Full bathrooms", full, "field_prop_baths_full"),
            ("Half bathrooms", half, "field_prop_baths_half"),
            ("Square footage", f"{sqft:,} sq ft", "field_prop_sqft"),
            ("Levels", levels, "field_prop_levels"),
            ("Lot size", d["lot"], "field_prop_lot_size"),
            ("Year built", d["year"], "field_prop_year_built"),
            ("Property type", ptype, "field_prop_type"),
        ]),
        ("Systems and condition", [
            ("Roof age", f"{d['roof']} years", "field_prop_roof_age"),
            ("HVAC age", f"{(i * 5) % 18 + 1} years", "field_prop_hvac_age"),
            ("Water heater age", f"{(i * 4) % 14 + 1} years", "field_prop_wh_age"),
            ("Windows", _spoken(_WINDOWS[i % len(_WINDOWS)]), "field_prop_windows"),
            ("Counters", _spoken(_COUNTER[i % len(_COUNTER)]), "field_prop_counter"),
            ("Garage", _spoken(garage) + (f" · {garage_spaces} car" if garage_spaces else ""),
             "field_prop_garage_type field_prop_garage_spaces"),
        ]),
        ("Plumbing, electrical, structure", [
            ("Supply lines", _spoken(_SUPPLY[i % len(_SUPPLY)]), "field_prop_supply_line"),
            ("Drain lines", _spoken(_SUPPLY[(i + 2) % len(_SUPPLY)]), "field_prop_drain_line"),
            ("Sewer", _spoken(_SEWER[ptype]), "field_prop_sewer"),
            ("Wiring", _spoken(_WIRING[i % len(_WIRING)]), "field_prop_wiring"),
            ("Electrical panel", _spoken(_PANEL[i % len(_PANEL)]), "field_prop_panel_size"),
            ("Foundation", _spoken(foundation), "field_prop_foundation"),
            ("Structure", _spoken(_STRUCTURE[i % len(_STRUCTURE)]), "field_prop_structure"),
        ]),
        ("What it costs to run", [
            ("Annual property tax", money(d["tax"]), "field_prop_taxes"),
            ("Annual insurance", money(insurance), "field_prop_insurance"),
            ("HOA fee", (money(d["hoa"]) + " / month") if d["hoa"] else "None",
             "field_prop_hoa_yn field_prop_hoa_fee field_prop_hoa_freq"),
            ("Electricity", money(electric) + " / month", "field_prop_util_electric"),
            ("Gas", money(gas) + " / month", "field_prop_util_gas"),
            ("Water and sewer", money(water) + " / month", "field_prop_util_water"),
            ("Trash", money(trash) + " / month", "field_prop_util_trash"),
        ]),
        ("Location", [
            ("Neighborhood", hood, "field_prop_neighborhood"),
            ("Walk score", f"{walk} / 100", "field_prop_walk_score"),
            ("Flood zone", _spoken("x_unshaded" if walk > 70 else "x_shaded"),
             "field_prop_flood_zone"),
            ("Price per sq ft", f"${d['ppsf']}", "field_prop_price"),
        ]),
    ]
