#!/usr/bin/env python3
import random
import os
import sys
import tty
import termios
import re
import select
import signal
import json
import uuid
from datetime import datetime

# ANSI color codes
GREEN = "\033[1;32m"
RED = "\033[1;31m"
YELLOW = "\033[1;33m"
CYAN = "\033[1;36m"
DIM = "\033[2m"
BOLD = "\033[1m"
RESET = "\033[0m"
WHITE = "\033[1;37m"

# Per-type cell colors: (foreground, background)
# Bright fg on a muted/dark-tinted bg of the same hue
CELL_COLORS = {
    "treasure":      ("\033[1;38;5;220m", "\033[48;5;58m"),   # bright gold fg, dark yellow bg
    "trap":          ("\033[1;38;5;196m", "\033[48;5;52m"),    # vivid red fg, dark red bg
    "near_treasure": ("\033[1;38;5;82m",  "\033[48;5;22m"),   # bright green fg, dark green bg
    "near_trap":     ("\033[1;38;5;203m", "\033[48;5;52m"),    # bright red fg, dark red bg
}

GRID_SIZE = 8
NUM_TREASURES = 5
NUM_TRAPS = 4
MAX_DIGS = 30
GOLD_VALUES = [30, 40, 50, 60, 80]
TRAP_PENALTY_DIGS = 3
TRAP_PENALTY_GOLD_FRAC = 0.2
GOAL_GOLD = 200

CELL_W = 5

# Left margin: "  " (2) + row_num (1) + "  " (2) + border (1) = 6 chars before first cell
GRID_LEFT_COL = 6     # 0-indexed
ROW_STRIDE = 2
COL_STRIDE = CELL_W + 1

# Flag set by SIGWINCH handler
_resize_pending = False

# --- Game Logger ---
LOG_DIR = os.path.join(os.path.expanduser("~"), ".lantern-games", "logs")
LOG_FILE = os.path.join(LOG_DIR, "treasure_hunt.jsonl")


class GameLogger:
    def __init__(self):
        self.game_id = str(uuid.uuid4())[:8]
        self.start_time = datetime.now().isoformat()
        self.move_number = 0
        os.makedirs(LOG_DIR, exist_ok=True)

    def _write(self, entry):
        entry["game_id"] = self.game_id
        entry["timestamp"] = datetime.now().isoformat()
        with open(LOG_FILE, "a") as f:
            f.write(json.dumps(entry) + "\n")

    def log_game_start(self, treasures, treasure_gold, traps):
        self._write({
            "event": "game_start",
            "grid_size": GRID_SIZE,
            "num_treasures": NUM_TREASURES,
            "num_traps": NUM_TRAPS,
            "max_digs": MAX_DIGS,
            "goal_gold": GOAL_GOLD,
            "treasures": [{"pos": [r, c], "gold": treasure_gold[(r, c)]} for r, c in treasures],
            "traps": [[r, c] for r, c in traps],
        })

    def log_move(self, r, c, outcome, details=None):
        self.move_number += 1
        entry = {
            "event": "move",
            "move_number": self.move_number,
            "cell": [r, c],
            "cell_label": f"{'abcdefgh'[c]}{r + 1}",
            "outcome": outcome,
        }
        if details:
            entry.update(details)
        self._write(entry)

    def log_game_end(self, result, gold, digs_left, dug_count):
        self._write({
            "event": "game_end",
            "result": result,
            "final_gold": gold,
            "digs_left": digs_left,
            "cells_dug": dug_count,
            "total_moves": self.move_number,
        })


def manhattan(r1, c1, r2, c2):
    return abs(r1 - r2) + abs(c1 - c2)


def place_items(count, existing):
    items = []
    while len(items) < count:
        r = random.randint(0, GRID_SIZE - 1)
        c = random.randint(0, GRID_SIZE - 1)
        if (r, c) not in existing and (r, c) not in items:
            items.append((r, c))
    return items


def pad_cell(content, width=CELL_W):
    dw = display_width(content)
    if dw >= width:
        return content
    total_pad = width - dw
    left = total_pad // 2
    right = total_pad - left
    return " " * left + content + " " * right


def display_width(s):
    import unicodedata
    clean = re.sub(r'\033\[[0-9;]*m', '', s)
    w = 0
    for ch in clean:
        cat = unicodedata.east_asian_width(ch)
        if cat in ('W', 'F'):
            w += 2
        else:
            w += 1
    return w


def write(s):
    sys.stdout.write(s)
    sys.stdout.flush()


def enable_mouse():
    write("\033[?1000h\033[?1006h")


def disable_mouse():
    write("\033[?1000l\033[?1006l")


def hide_cursor():
    write("\033[?25l")


def show_cursor():
    write("\033[?25h")


def enter_alt_screen():
    write("\033[?1049h")


def exit_alt_screen():
    write("\033[?1049l")


def terminal_to_grid(term_row, term_col, grid_top_row):
    """Convert 1-based terminal coordinates to 0-based grid (row, col), or None."""
    tr = term_row - 1  # to 0-based
    tc = term_col - 1

    row_offset = tr - grid_top_row
    if row_offset < 0 or row_offset % ROW_STRIDE != 0:
        return None
    grid_row = row_offset // ROW_STRIDE
    if grid_row < 0 or grid_row >= GRID_SIZE:
        return None

    col_offset = tc - GRID_LEFT_COL
    if col_offset < 0:
        return None
    cell_index = col_offset // COL_STRIDE
    pos_in_cell = col_offset % COL_STRIDE
    if pos_in_cell >= CELL_W:
        return None
    if cell_index < 0 or cell_index >= GRID_SIZE:
        return None

    return (grid_row, cell_index)


def render(grid, dug, gold, digs_left, message="", clicked=None):
    cols = "abcdefgh"[:GRID_SIZE]

    # Clear alt screen, scrollback, and go home
    write("\033[H\033[2J\033[3J")

    banner_w = GRID_SIZE * CELL_W + GRID_SIZE + 1
    inner_w = banner_w - 2

    lines = []
    lines.append("")
    lines.append(f"  {BOLD}\u250c{'\u2500' * inner_w}\u2510{RESET}")

    title = "T R E A S U R E   H U N T"
    title_pad = inner_w - len(title)
    title_left = title_pad // 2
    title_right = title_pad - title_left
    lines.append(f"  {BOLD}\u2502{RESET}{' ' * title_left}{YELLOW}{title}{RESET}{' ' * title_right}{BOLD}\u2502{RESET}")

    lines.append(f"  {BOLD}\u2502{' ' * inner_w}\u2502{RESET}")

    digs_color = GREEN if digs_left > 10 else (YELLOW if digs_left > 5 else RED)
    gold_color = GREEN if gold >= GOAL_GOLD else YELLOW
    digs_plain = f"Digs left: {digs_left}"
    gold_plain = f"Gold: {gold} / {GOAL_GOLD}"
    gap = inner_w - 4 - len(digs_plain) - len(gold_plain)
    status = f"  Digs left: {digs_color}{digs_left}{RESET}{' ' * gap}Gold: {gold_color}{gold}{RESET} / {GOAL_GOLD}  "
    lines.append(f"  {BOLD}\u2502{RESET}{status}{BOLD}\u2502{RESET}")

    lines.append(f"  {BOLD}\u2514{'\u2500' * inner_w}\u2518{RESET}")
    lines.append("")

    # Column headers
    header = "     "
    for i, c in enumerate(cols):
        cell_content = pad_cell(f"{BOLD}{c}{RESET}", CELL_W)
        if i == 0:
            header += " "
        header += cell_content
        header += " "
    lines.append(header)

    # Top border
    top = "\u250c" + ("\u2500" * CELL_W + "\u252c") * (GRID_SIZE - 1) + "\u2500" * CELL_W + "\u2510"
    lines.append(f"  {DIM}   {top}{RESET}")
    grid_top_row = len(lines)

    for r in range(GRID_SIZE):
        row_str = f"  {BOLD}{r + 1}{RESET}  {DIM}\u2502{RESET}"
        for c in range(GRID_SIZE):
            cell = grid[r][c]
            if (r, c) not in dug:
                content = f"{DIM}\u2591\u2591{RESET}"
                row_str += pad_cell(content, CELL_W) + f"{DIM}\u2502{RESET}"
            else:
                was_clicked = clicked is None or (r, c) in clicked
                if cell == "treasure":
                    if was_clicked:
                        fg, bg = CELL_COLORS["treasure"]
                        symbol = f"{fg}${RESET}{bg}"
                    else:
                        symbol = f"{YELLOW}${RESET}"
                elif cell == "trap":
                    if was_clicked:
                        fg, bg = CELL_COLORS["trap"]
                        symbol = f"{fg}*{RESET}{bg}"
                    else:
                        symbol = f"{RED}*{RESET}"
                else:
                    dist, nearest_type = cell
                    if was_clicked:
                        key = "near_treasure" if nearest_type == "treasure" else "near_trap"
                        fg, bg = CELL_COLORS[key]
                        symbol = f"{fg}{dist}{RESET}{bg}"
                    else:
                        fg = GREEN if nearest_type == "treasure" else RED
                        symbol = f"{fg}{dist}{RESET}"
                if was_clicked:
                    padded = pad_cell(symbol, CELL_W)
                    row_str += f"{bg}{padded}{RESET}{DIM}\u2502{RESET}"
                else:
                    row_str += pad_cell(symbol, CELL_W) + f"{DIM}\u2502{RESET}"
        lines.append(row_str)

        if r < GRID_SIZE - 1:
            mid = "\u251c" + ("\u2500" * CELL_W + "\u253c") * (GRID_SIZE - 1) + "\u2500" * CELL_W + "\u2524"
            lines.append(f"  {DIM}   {mid}{RESET}")
        else:
            bot = "\u2514" + ("\u2500" * CELL_W + "\u2534") * (GRID_SIZE - 1) + "\u2500" * CELL_W + "\u2518"
            lines.append(f"  {DIM}   {bot}{RESET}")

    lines.append("")
    lines.append(f"  {BOLD}{CYAN}GOAL{RESET}")
    lines.append(f"  {DIM}-{RESET} Collect {BOLD}{GOAL_GOLD} gold{RESET} before you run out of digs")
    lines.append("")
    lines.append(f"  {BOLD}{CYAN}SYMBOLS{RESET}")
    lines.append(f"  {DIM}-{RESET} {GREEN}3{RESET}  nearest treasure is 3 steps away")
    lines.append(f"  {DIM}-{RESET} {RED}2{RESET}  nearest trap is 2 steps away")
    lines.append(f"  {DIM}-{RESET} {YELLOW}${RESET}  found treasure!")
    lines.append(f"  {DIM}-{RESET} {RED}*{RESET}  hit a trap! (-{TRAP_PENALTY_DIGS} digs, -{int(TRAP_PENALTY_GOLD_FRAC*100)}% gold)")
    lines.append("")
    lines.append(f"  {BOLD}{CYAN}HOW TO PLAY{RESET}")
    lines.append(f"  {DIM}-{RESET} Click a cell or type a coordinate (e.g. d4)")
    lines.append(f"  {DIM}-{RESET} Distance = steps up/down/left/right (not diagonal)")
    lines.append(f"  {DIM}-{RESET} q to quit")
    lines.append("")
    if message:
        lines.append(f"  {message}")
    else:
        lines.append(f"  {DIM}Dig somewhere...{RESET}")
    lines.append("")

    write("\r\n".join(lines))

    # Account for scrolling: if content is taller than terminal,
    # the viewport shifted up, so grid's visual row is lower than its line index
    try:
        term_h = os.get_terminal_size().lines
    except OSError:
        term_h = 50
    total_lines = len(lines)
    scroll = max(0, total_lines - term_h)
    visual_grid_top = grid_top_row - scroll  # 0-based visual row in viewport

    return visual_grid_top


def read_key(fd):
    """Read a keypress or mouse event."""
    ch = os.read(fd, 1)
    if not ch:
        return None

    if ch == b'\x1b':
        buf = b''
        while True:
            if select.select([fd], [], [], 0.05)[0]:
                buf += os.read(fd, 1)
            else:
                break

        seq = buf.decode('utf-8', errors='replace')

        # SGR mouse: \033[<Cb;Cx;CyM or \033[<Cb;Cx;Cym
        m = re.match(r'\[<(\d+);(\d+);(\d+)([Mm])', seq)
        if m:
            button = int(m.group(1))
            col = int(m.group(2))
            row = int(m.group(3))
            released = m.group(4) == 'm'
            if not released and button == 0:
                return ('mouse', 0, col, row)
            return ('ignore',)

        return ('key', '\x1b')

    b = ch[0]

    if b == 3 or b == 4:
        return ('quit',)

    if b == ord('q') or b == ord('Q'):
        return ('quit',)

    if 0x20 <= b < 0x7f:
        text = chr(b)
        while select.select([fd], [], [], 0.1)[0]:
            more = os.read(fd, 1)
            if more and 0x20 <= more[0] < 0x7f:
                text += chr(more[0])
            elif more and more[0] in (10, 13):
                break
            else:
                break
        return ('text', text.strip())

    if b in (10, 13):
        return ('enter',)

    return ('ignore',)


def parse_coordinate(text):
    cols = "abcdefgh"[:GRID_SIZE]
    text = text.strip().lower()
    if len(text) < 2:
        return None
    col_ch = text[0]
    row_str = text[1:]
    if col_ch not in cols:
        return None
    try:
        row_num = int(row_str)
    except ValueError:
        return None
    if row_num < 1 or row_num > GRID_SIZE:
        return None
    return (row_num - 1, cols.index(col_ch))


def get_reading(r, c, treasures, traps):
    min_t_dist = min(manhattan(r, c, tr, tc) for tr, tc in treasures) if treasures else 999
    min_x_dist = min(manhattan(r, c, xr, xc) for xr, xc in traps) if traps else 999
    if min_t_dist <= min_x_dist:
        return (min_t_dist, "treasure")
    else:
        return (min_x_dist, "trap")


def stars(gold, digs_left):
    if digs_left >= 15:
        return f"{YELLOW}*** Master treasure hunter!{RESET}"
    elif digs_left >= 8:
        return f"{YELLOW}**{RESET}{DIM}*{RESET}  Skilled explorer!"
    else:
        return f"{YELLOW}*{RESET}{DIM}**{RESET}  You found it... barely!"


def reveal_all(grid, dug, treasures_remaining, traps_remaining):
    for r, c in treasures_remaining:
        grid[r][c] = "treasure"
        dug.add((r, c))
    for r, c in traps_remaining:
        grid[r][c] = "trap"
        dug.add((r, c))


def wait_for_quit(fd):
    """Block until the user presses q or Ctrl-C/Ctrl-D."""
    while True:
        if not select.select([fd], [], [], 0.2)[0]:
            continue
        event = read_key(fd)
        if event is None or event[0] == 'quit':
            return


def main():
    global _resize_pending

    logger = GameLogger()

    treasures = place_items(NUM_TREASURES, set())
    treasure_gold = {t: g for t, g in zip(treasures, sorted(GOLD_VALUES, reverse=True))}
    traps = place_items(NUM_TRAPS, set(treasures))

    logger.log_game_start(treasures, treasure_gold, traps)

    grid = [[None for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
    dug = set()
    clicked = set()  # cells the player actually dug (not revealed at end)
    gold = 0
    digs_left = MAX_DIGS
    treasures_remaining = list(treasures)
    traps_remaining = list(traps)
    cols = "abcdefgh"[:GRID_SIZE]
    message = ""

    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)

    def on_resize(signum, frame):
        global _resize_pending
        _resize_pending = True

    try:
        tty.setraw(fd)
        enter_alt_screen()
        enable_mouse()
        hide_cursor()
        signal.signal(signal.SIGWINCH, on_resize)

        grid_top = render(grid, dug, gold, digs_left, message)

        while digs_left > 0:
            # Handle resize
            if _resize_pending:
                _resize_pending = False
                grid_top = render(grid, dug, gold, digs_left, message)
                continue

            # Use select so we can check resize flag periodically
            if not select.select([fd], [], [], 0.1)[0]:
                continue

            event = read_key(fd)
            if event is None or event[0] == 'quit':
                logger.log_game_end("quit", gold, digs_left, len(dug))
                break

            if event[0] == 'ignore' or event[0] == 'enter':
                continue

            target = None

            if event[0] == 'mouse':
                _, button, term_col, term_row = event
                result = terminal_to_grid(term_row, term_col, grid_top)
                if result:
                    target = result
                else:
                    continue

            elif event[0] == 'text':
                result = parse_coordinate(event[1])
                if result:
                    target = result
                else:
                    message = f"{DIM}Type a coordinate like c4, or click a cell{RESET}"
                    grid_top = render(grid, dug, gold, digs_left, message)
                    continue

            elif event[0] == 'key':
                continue

            if target is None:
                continue

            r, c = target

            if (r, c) in dug:
                message = f"{DIM}Already dug there!{RESET}"
                grid_top = render(grid, dug, gold, digs_left, message)
                continue

            # Dig!
            dug.add((r, c))
            clicked.add((r, c))
            digs_left -= 1

            if (r, c) in treasures_remaining:
                grid[r][c] = "treasure"
                earned = treasure_gold[(r, c)]
                gold += earned
                treasures_remaining.remove((r, c))
                logger.log_move(r, c, "treasure", {
                    "gold_earned": earned, "gold_after": gold,
                    "digs_left": digs_left, "treasures_remaining": len(treasures_remaining),
                })
                message = f"{YELLOW}* Found treasure worth {earned} gold!{RESET}"
                grid_top = render(grid, dug, gold, digs_left, message)
                if not treasures_remaining:
                    reveal_all(grid, dug, treasures_remaining, traps_remaining)
                    msg = f"{GREEN}{BOLD}ALL TREASURES FOUND! Final gold: {gold} with {digs_left} digs remaining!{RESET}"
                    end_msg = msg + "\r\n  " + stars(gold, digs_left) + f"\r\n\r\n  {DIM}Press q to exit{RESET}"
                    render(grid, dug, gold, digs_left, end_msg, clicked=clicked)
                    logger.log_game_end("all_treasures_found", gold, digs_left, len(dug))
                    wait_for_quit(fd)
                    return
            elif (r, c) in traps_remaining:
                grid[r][c] = "trap"
                traps_remaining.remove((r, c))
                lost_gold = int(gold * TRAP_PENALTY_GOLD_FRAC)
                gold -= lost_gold
                digs_left = max(0, digs_left - TRAP_PENALTY_DIGS)
                logger.log_move(r, c, "trap", {
                    "gold_lost": lost_gold, "digs_lost": TRAP_PENALTY_DIGS,
                    "gold_after": gold, "digs_left": digs_left,
                    "traps_remaining": len(traps_remaining),
                })
                message = f"{RED}x Trap! Lost {lost_gold} gold and {TRAP_PENALTY_DIGS} digs.{RESET}"
                grid_top = render(grid, dug, gold, digs_left, message)
            else:
                reading = get_reading(r, c, treasures_remaining, traps_remaining)
                grid[r][c] = reading
                logger.log_move(r, c, "empty", {
                    "nearest_distance": reading[0], "nearest_type": reading[1],
                    "gold": gold, "digs_left": digs_left,
                })
                message = ""
                grid_top = render(grid, dug, gold, digs_left, message)

            # Check win
            if gold >= GOAL_GOLD:
                reveal_all(grid, dug, treasures_remaining, traps_remaining)
                msg = f"{GREEN}{BOLD}TARGET REACHED! Gold: {gold} with {digs_left} digs remaining!{RESET}"
                end_msg = msg + "\r\n  " + stars(gold, digs_left) + f"\r\n\r\n  {DIM}Press q to exit{RESET}"
                render(grid, dug, gold, digs_left, end_msg, clicked=clicked)
                logger.log_game_end("target_reached", gold, digs_left, len(dug))
                wait_for_quit(fd)
                return

        # Out of digs
        if digs_left <= 0:
            reveal_all(grid, dug, treasures_remaining, traps_remaining)
            msg = f"{RED}Out of digs! Final gold: {gold} / {GOAL_GOLD}{RESET}"
            end_msg = msg + f"\r\n\r\n  {DIM}Press q to exit{RESET}"
            render(grid, dug, gold, digs_left, end_msg, clicked=clicked)
            logger.log_game_end("out_of_digs", gold, digs_left, len(dug))
            wait_for_quit(fd)

    finally:
        signal.signal(signal.SIGWINCH, signal.SIG_DFL)
        disable_mouse()
        show_cursor()
        exit_alt_screen()
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)


if __name__ == "__main__":
    main()
