import curses
import time
import argparse
import sys
import locale
from datetime import date, timedelta

from desire_cli.sentences import generate, load_daily_statement, save_daily_statement
from desire_cli.update import get_update_info
from desire_cli.profile import (
    profile_exists, read_profile, create_profile, append_statement,
    get_theme, set_theme,
)
from desire_cli.themes import THEMES, THEME_NAMES


locale.setlocale(locale.LC_ALL, "")

# ── colors ───────────────────────────────────────────────────────────────────

C_DIM = 1
C_OK = 2
C_ERR = 3
C_CURSOR = 4
C_ACCENT = 5
C_STAT = 6
C_TITLE = 7
C_BORDER = 8
C_GOOD = 9
C_BAD = 10
C_HINT = 11

DEFAULT_SOURCE_ID = "__legacy__"


def _date_label(day):
    return day.strftime("%Y-%m-%d")


def init_colors(theme_name="default"):
    curses.start_color()
    curses.use_default_colors()
    theme = THEMES.get(theme_name, THEMES["default"])
    for pair_id, (fg, bg) in theme.items():
        curses.init_pair(pair_id, fg, bg)


# ── draw helpers ─────────────────────────────────────────────────────────────

def cx(w, n):
    return max(0, (w - n) // 2)


def put(win, y, x, text, cp=0, attr=0):
    try:
        win.addstr(y, x, text, curses.color_pair(cp) | attr)
    except curses.error:
        pass


def putc(win, y, w, text, cp=0, attr=0):
    put(win, y, cx(w, len(text)), text, cp, attr)


def hline(win, y, x, n, cp=C_BORDER):
    put(win, y, x, "-" * n, cp)


def wrap(text, width):
    lines, cur = [], ""
    for word in text.split(" "):
        test = f"{cur} {word}" if cur else word
        if len(test) <= width:
            cur = test
        else:
            if cur:
                lines.append(cur)
            cur = word
    if cur:
        lines.append(cur)
    return lines


# ── logo ─────────────────────────────────────────────────────────────────────

LOGO = [

"  ╔╦╗╔═╗╔╦╗",
"   ║║╚═╗ ║ ",
"  ═╩╝╚═╝ ╩ ",
"Desire Statement Typer"
]
LOGO_H = len(LOGO)


def draw_logo(win, y, w):
    for i, line in enumerate(LOGO):
        putc(win, y + i, w, line, C_ACCENT, curses.A_BOLD)


def _count_errors(target, typed):
    n = min(len(typed), len(target))
    return sum(1 for i in range(n) if typed[i] != target[i])


def _build_statement_counts(events, today_str):
    counts = {}
    for ev in events:
        key = ev.get("source_id") or DEFAULT_SOURCE_ID
        info = counts.setdefault(key, {"total": 0, "today": 0})
        info["total"] += 1
        ts = str(ev.get("ts", ""))[:10]
        if ts == today_str:
            info["today"] += 1
    return counts


def draw_stats(scr, y, w, total, daily, errs, label=None):
    label_str = str(label) if label else ""
    daily_str = str(daily)
    total_str = str(total)
    err_str = str(errs)
    spacer = "   "
    today_suffix = " today   "
    total_suffix = " total   "
    err_suffix = " err"
    label_len = len(label_str) + len(spacer) if label else 0
    bar_len = (
        label_len
        + len(daily_str)
        + len(today_suffix)
        + len(total_str)
        + len(total_suffix)
        + len(err_str)
        + len(err_suffix)
    )
    sx = cx(w, bar_len)
    px = sx
    if label:
        put(scr, y, px, label_str, C_TITLE, curses.A_BOLD)
        px += len(label_str)
        put(scr, y, px, spacer, C_DIM)
        px += len(spacer)
    put(scr, y, px, daily_str, C_STAT, curses.A_BOLD)
    px += len(daily_str)
    put(scr, y, px, today_suffix, C_DIM)
    px += len(today_suffix)
    put(scr, y, px, total_str, C_ACCENT, curses.A_BOLD)
    px += len(total_str)
    put(scr, y, px, total_suffix, C_DIM)
    px += len(total_suffix)
    ec = C_BAD if errs > 0 else C_GOOD
    put(scr, y, px, err_str, ec, curses.A_BOLD)
    px += len(err_str)
    put(scr, y, px, err_suffix, C_DIM)


# ── text drawing ─────────────────────────────────────────────────────────────

def draw_text(scr, lines, typed, target, sy, sx, aw, sh):
    pos = len(typed)
    maxl = sh - sy - 3
    cline, ci = 0, 0
    for i, line in enumerate(lines):
        if ci + len(line) >= pos:
            cline = i
            break
        ci += len(line) + 1

    vis = min(maxl, 3)
    ss = max(0, min(cline - 1, len(lines) - vis))
    off = sum(len(lines[i]) + 1 for i in range(ss))

    cursor_pos = None
    for li in range(ss, min(ss + vis, len(lines))):
        line = lines[li]
        y = sy + (li - ss)
        if y >= sh - 2:
            break
        for ci2, ch in enumerate(line):
            ri = off + ci2
            x = sx + ci2
            if x >= sx + aw:
                break
            if ri < pos:
                cp = C_OK if ri < len(typed) and typed[ri] == target[ri] else C_ERR
                at = curses.A_UNDERLINE if cp == C_ERR else 0
                put(scr, y, x, ch, cp, at)
            elif ri == pos:
                put(scr, y, x, ch, C_DIM)
                cursor_pos = (y, x)
            else:
                put(scr, y, x, ch, C_DIM)
        off += len(line) + 1
    return cursor_pos


def draw_editor(scr, text_lines, row, col, text_y, text_x, aw, h):
    max_vis_lines = max(3, h - text_y - 3)
    start = 0
    if row >= max_vis_lines:
        start = row - max_vis_lines + 1

    visible = text_lines[start:start + max_vis_lines]
    for i, line in enumerate(visible):
        y = text_y + i
        if y >= h - 2:
            break
        chunk = line[:aw]
        put(scr, y, text_x, chunk, C_TITLE)

    cursor_y = text_y + (row - start)
    cursor_x = text_x + min(col, aw - 1)
    return cursor_y, cursor_x


# ── main test loop ───────────────────────────────────────────────────────────

def test(scr, update_info=None, theme_name="default"):
    curses.curs_set(0)
    scr.nodelay(True)
    scr.timeout(50)
    curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)

    mode = "normal"
    daily_day = date.today()

    statement_entry = None
    target = ""
    source_id = DEFAULT_SOURCE_ID
    source_label = "statements"
    typed = []
    started = False
    daily_raw_text = ""
    editing = False
    editor_lines = [""]
    editor_row = 0
    editor_col = 0
    status_msg = ""
    status_until = 0.0

    stats_y = 0
    hint_y = 0
    today_str = time.strftime("%Y-%m-%d")
    profile = read_profile()
    user_name = profile.get("name", "")
    statements = profile.get("statements", [])
    statement_counts = _build_statement_counts(statements, today_str)
    file_counts = statement_counts.setdefault(source_id, {"total": 0, "today": 0})
    total_count = file_counts["total"]
    daily_count = file_counts["today"]
    completion_logged = False

    def _set_editor(raw_text):
        nonlocal editor_lines
        nonlocal editor_row
        nonlocal editor_col
        editor_lines = raw_text.splitlines() or [""]
        editor_row = 0
        editor_col = 0

    def _editor_to_raw():
        return "\n".join(editor_lines)

    def _load_statement(ensure_daily=False):
        nonlocal statement_entry
        nonlocal target
        nonlocal source_id
        nonlocal source_label
        nonlocal typed
        nonlocal started
        nonlocal completion_logged
        nonlocal file_counts
        nonlocal total_count
        nonlocal daily_count
        nonlocal daily_raw_text

        if mode == "daily":
            statement_entry, daily_raw_text, _ = load_daily_statement(daily_day, ensure=ensure_daily)
        else:
            statement_entry = generate(1, "medium")
            daily_raw_text = ""

        target = statement_entry.text
        source_id = statement_entry.source_id or DEFAULT_SOURCE_ID
        source_label = statement_entry.source_label or "statements"
        typed = []
        started = False
        completion_logged = False
        file_counts = statement_counts.setdefault(source_id, {"total": 0, "today": 0})
        total_count = file_counts["total"]
        daily_count = file_counts["today"]

    _load_statement()

    while True:
        scr.erase()
        h, w = scr.getmaxyx()
        if h < 10 or w < 40:
            scr.addstr(0, 0, "terminal too small!")
            scr.refresh()
            if scr.getch() == 27:  # esc
                return None, theme_name
            continue

        # ── responsive text width ──
        aw = min(w - 6, 90)
        lines = wrap(target, aw) if target else [""]
        text_vis = min(3, len(lines))

        # ── compute layout — text stays fixed at vertical center ──
        text_y = max(1, (h - text_vis) // 2)

        logo_y = text_y - LOGO_H - 3
        stats_y = text_y - 2
        hint_y = text_y + text_vis + 1

        tx = cx(w, aw)

        # ── draw ──
        mode_hint = "daily" if mode == "daily" else "random"
        if mode == "daily":
            mode_hint = f"{mode_hint} {_date_label(daily_day)}"

        if editing:
            draw_logo(scr, logo_y, w)
            putc(scr, stats_y, w, f"editing {source_label}", C_TITLE, curses.A_BOLD)
            cpos = draw_editor(scr, editor_lines, editor_row, editor_col, text_y, tx, aw, h)
            putc(scr, hint_y, w, "ctrl+s save   esc done   arrows move   enter newline", C_HINT)
            putc(scr, h - 1, w, f"d mode ({mode_hint})   left/right day", C_HINT)
        elif not started:
            draw_logo(scr, logo_y, w)
            cpos = draw_text(scr, lines, typed, target, text_y, tx, aw, h)
            if mode == "daily" and not target:
                putc(scr, hint_y, w, "daily file is empty - press e to edit", C_DIM)
            else:
                putc(scr, hint_y, w, "start typing...", C_DIM)
            if update_info and update_info["update_available"]:
                notice = f"v{update_info['latest']} available: {update_info['update_cmd']}"
                putc(scr, h - 2, w, notice, C_BAD)
            if mode == "daily":
                putc(scr, h - 1, w, "e edit   left/right day   d mode   s stats   t theme   tab refresh   esc quit", C_HINT)
            else:
                putc(scr, h - 1, w, "d daily mode   s stats   t theme   tab new statement   esc quit", C_HINT)
            ver = f"v{update_info['version']}" if update_info else ""
            if ver:
                put(scr, h - 1, 1, ver, C_DIM)
                put(scr, h - 1, 1 + len(ver) + 2, theme_name, C_DIM)
                put(scr, h - 1, 1 + len(ver) + len(theme_name) + 4, mode_hint, C_DIM)
            if user_name:
                put(scr, h - 1, w - len(user_name) - 1, user_name, C_DIM)
        else:
            errs = _count_errors(target, typed)
            draw_stats(scr, stats_y, w, total_count, daily_count, errs, source_label)
            cpos = draw_text(scr, lines, typed, target, text_y, tx, aw, h)
            putc(scr, h - 1, w, "tab new statement   esc quit", C_HINT)

        if status_msg and time.time() <= status_until:
            putc(scr, h - 2, w, status_msg, C_GOOD)

        # show line cursor at current typing position
        if cpos:
            try:
                sys.stdout.write("\033[6 q")  # steady bar cursor
                sys.stdout.flush()
                curses.curs_set(1)
                scr.move(cpos[0], cpos[1])
            except curses.error:
                pass
        else:
            sys.stdout.write("\033[0 q")  # restore default cursor
            sys.stdout.flush()
            curses.curs_set(0)

        scr.refresh()

        # ── input ──
        try:
            k = scr.get_wch()
        except curses.error:
            continue

        if k in (9, "\t"):  # tab
            editing = False
            if mode == "daily":
                _load_statement(ensure_daily=True)
                if not target:
                    _set_editor(daily_raw_text)
                    editing = True
            else:
                _load_statement()
            continue

        if editing:
            if k in (19,):
                daily_raw_text = _editor_to_raw()
                statement_entry = save_daily_statement(daily_day, daily_raw_text)
                target = statement_entry.text
                source_id = statement_entry.source_id or DEFAULT_SOURCE_ID
                source_label = statement_entry.source_label or "statements"
                file_counts = statement_counts.setdefault(source_id, {"total": 0, "today": 0})
                total_count = file_counts["total"]
                daily_count = file_counts["today"]
                status_msg = f"saved {source_label}"
                status_until = time.time() + 1.5
                continue
            if k in (27, "\x1b"):
                editing = False
                continue
            if k in (curses.KEY_LEFT,):
                if editor_col > 0:
                    editor_col -= 1
                elif editor_row > 0:
                    editor_row -= 1
                    editor_col = len(editor_lines[editor_row])
                continue
            if k in (curses.KEY_RIGHT,):
                if editor_col < len(editor_lines[editor_row]):
                    editor_col += 1
                elif editor_row < len(editor_lines) - 1:
                    editor_row += 1
                    editor_col = 0
                continue
            if k in (curses.KEY_UP,):
                if editor_row > 0:
                    editor_row -= 1
                    editor_col = min(editor_col, len(editor_lines[editor_row]))
                continue
            if k in (curses.KEY_DOWN,):
                if editor_row < len(editor_lines) - 1:
                    editor_row += 1
                    editor_col = min(editor_col, len(editor_lines[editor_row]))
                continue
            if k in (10, 13, "\n"):
                cur = editor_lines[editor_row]
                left = cur[:editor_col]
                right = cur[editor_col:]
                editor_lines[editor_row] = left
                editor_lines.insert(editor_row + 1, right)
                editor_row += 1
                editor_col = 0
                continue
            if k in (curses.KEY_BACKSPACE, 127, 8, "\b", "\x7f"):
                if editor_col > 0:
                    cur = editor_lines[editor_row]
                    editor_lines[editor_row] = cur[:editor_col - 1] + cur[editor_col:]
                    editor_col -= 1
                elif editor_row > 0:
                    prev = editor_lines[editor_row - 1]
                    cur = editor_lines.pop(editor_row)
                    editor_row -= 1
                    editor_col = len(prev)
                    editor_lines[editor_row] = prev + cur
                continue
            if k in (curses.KEY_DC,):
                cur = editor_lines[editor_row]
                if editor_col < len(cur):
                    editor_lines[editor_row] = cur[:editor_col] + cur[editor_col + 1:]
                elif editor_row < len(editor_lines) - 1:
                    editor_lines[editor_row] = cur + editor_lines.pop(editor_row + 1)
                continue

            if isinstance(k, str) and len(k) == 1 and k.isprintable() and k not in ("\x1b",):
                cur = editor_lines[editor_row]
                editor_lines[editor_row] = cur[:editor_col] + k + cur[editor_col:]
                editor_col += 1
            elif isinstance(k, int) and 32 <= k <= 126:
                ch = chr(k)
                cur = editor_lines[editor_row]
                editor_lines[editor_row] = cur[:editor_col] + ch + cur[editor_col:]
                editor_col += 1
            continue

        if not started:
            if k in (27, "\x1b"):  # esc
                sys.stdout.write("\033[0 q")
                sys.stdout.flush()
                return None, theme_name
            if k in (ord('s'), 's'):
                return "stats", theme_name
            if k in (ord('t'), 't'):
                idx = THEME_NAMES.index(theme_name)
                theme_name = THEME_NAMES[(idx + 1) % len(THEME_NAMES)]
                init_colors(theme_name)
                set_theme(theme_name)
                continue
            if k in (ord('d'), 'd'):
                if mode == "daily":
                    mode = "normal"
                    editing = False
                    _load_statement()
                else:
                    mode = "daily"
                    daily_day = date.today()
                    _load_statement(ensure_daily=True)
                    if not target:
                        _set_editor(daily_raw_text)
                        editing = True
                continue
            if mode == "daily" and k in (ord('e'), 'e'):
                _set_editor(daily_raw_text)
                editing = True
                continue
            if mode == "daily" and k == curses.KEY_LEFT:
                daily_day = daily_day - timedelta(days=1)
                editing = False
                _load_statement(ensure_daily=True)
                if not target:
                    _set_editor(daily_raw_text)
                    editing = True
                continue
            if mode == "daily" and k == curses.KEY_RIGHT:
                daily_day = daily_day + timedelta(days=1)
                editing = False
                _load_statement(ensure_daily=True)
                if not target:
                    _set_editor(daily_raw_text)
                    editing = True
                continue

        # typing
        if target and not started and ((isinstance(k, str) and len(k) == 1 and k.isprintable() and k not in ("\x1b",)) or (isinstance(k, int) and 32 <= k <= 126)):
            started = True

        if k in (curses.KEY_BACKSPACE, 127, 8, "\b", "\x7f"):
            if typed:
                typed.pop()
        elif target and len(typed) < len(target):
            if isinstance(k, str) and len(k) == 1 and k.isprintable() and k not in ("\x1b",):
                typed.append(k)
            elif isinstance(k, int) and 32 <= k <= 126:
                typed.append(chr(k))

        completed_now = False
        if len(typed) == len(target):
            completed_now = "".join(typed) == target

        if completed_now and not completion_logged:
            append_statement(target, source_id=source_id, source_label=source_label)
            file_counts["total"] += 1
            total_count = file_counts["total"]
            if time.strftime("%Y-%m-%d") == today_str:
                file_counts["today"] += 1
            daily_count = file_counts["today"]
            completion_logged = True
        elif not completed_now:
            completion_logged = False

    sys.stdout.write("\033[0 q")
    sys.stdout.flush()
    return None, theme_name


# ── name prompt ──────────────────────────────────────────────────────────────

def name_prompt(scr, existing=""):
    curses.curs_set(1)
    scr.nodelay(False)
    scr.timeout(-1)

    buf = list(existing)
    MAX_LEN = 20

    while True:
        scr.erase()
        h, w = scr.getmaxyx()
        y = max(1, h // 2 - 3)

        title = "-- change name --" if existing else "-- welcome to desire statement typer --"
        putc(scr, y, w, title, C_TITLE, curses.A_BOLD)
        y += 2
        putc(scr, y, w, "enter your name:", C_DIM)
        y += 2

        name_str = "".join(buf)
        input_display = "> " + name_str
        ix = cx(w, len(input_display) + 1)
        put(scr, y, ix, ">", C_ACCENT, curses.A_BOLD)
        put(scr, y, ix + 2, name_str, C_TITLE)
        cursor_y, cursor_x = y, ix + 2 + len(name_str)

        y += 2
        if existing:
            putc(scr, y, w, "enter to confirm   esc cancel", C_HINT)
        else:
            putc(scr, y, w, "enter to confirm", C_HINT)

        # move cursor after all drawing so addstr doesn't override it
        try:
            scr.move(cursor_y, cursor_x)
        except curses.error:
            pass
        scr.refresh()

        k = scr.getch()
        if k == 27:
            curses.curs_set(0)
            return None
        elif k in (10, 13):
            name = "".join(buf).strip()
            if name:
                curses.curs_set(0)
                return name
        elif k in (curses.KEY_BACKSPACE, 127, 8):
            if buf:
                buf.pop()
        elif 32 <= k <= 126 and len(buf) < MAX_LEN:
            buf.append(chr(k))

    curses.curs_set(0)


# ── stats screen ─────────────────────────────────────────────────────────────

def show_stats(scr):
    from desire_cli.profile import read_profile, set_name, compute_stats

    curses.curs_set(0)
    scr.nodelay(False)
    scr.timeout(-1)

    while True:
        profile = read_profile()
        stats = compute_stats(profile)

        scr.erase()
        h, w = scr.getmaxyx()

        if h < 15 or w < 40:
            try:
                scr.addstr(0, 0, "terminal too small!")
            except curses.error:
                pass
            scr.refresh()
            k = scr.getch()
            if k in (27, ord('q'), ord('Q')):
                return
            continue

        dw = min(50, w - 10)
        lx = cx(w, dw)
        rx = lx + dw // 2

        content_h = 26
        y = max(1, (h - content_h) // 2)

        # title
        putc(scr, y, w, "-- stats --", C_TITLE, curses.A_BOLD)
        y += 2

        # name
        putc(scr, y, w, stats["name"] or "(no name)", C_ACCENT, curses.A_BOLD)
        y += 1
        putc(scr, y, w, f"member since {stats['member_since']}", C_DIM)
        y += 2

        # divider
        hline(scr, y, cx(w, dw), dw)
        y += 2

        if stats.get("statements_completed", 0) == 0:
            putc(scr, y, w, "no stats yet -- start typing!", C_DIM)
        else:
            label_width = 13
            summary_specs = [
                ("total count", str(stats.get("statements_completed", 0)), C_ACCENT),
                ("daily count", str(stats.get("statements_today", 0)), C_STAT),
            ]
            streak_val = stats.get("statements_streak", 0)
            streak_str = f"{streak_val} day{'s' if streak_val != 1 else ''}"
            summary_specs.append(("streak", streak_str, C_ACCENT if streak_val > 0 else C_DIM))

            for text, value, color in summary_specs:
                put(scr, y, lx, text.ljust(label_width), C_DIM)
                put(scr, y, lx + label_width + 1, value, color, curses.A_BOLD)
                y += 1

            y += 1

            plot = stats.get("daily_plot", "")
            if plot:
                put(scr, y, lx, "last 30d".ljust(label_width), C_DIM)
                put(scr, y, lx + label_width + 1, plot, C_ACCENT, curses.A_BOLD)
                y += 1

                start_label = stats.get("daily_plot_start", "")
                end_label = stats.get("daily_plot_end", "")
                axis_x = lx + label_width + 1
                if start_label:
                    put(scr, y, axis_x, start_label, C_DIM)
                if end_label and len(plot) >= len(end_label):
                    put(scr, y, axis_x + len(plot) - len(end_label), end_label, C_DIM)
                y += 1

                max_day = stats.get("daily_plot_max", 0)
                put(scr, y, lx, "max/day".ljust(label_width), C_DIM)
                put(scr, y, lx + label_width + 1, str(max_day), C_STAT)
                y += 2

            # divider
            hline(scr, y, cx(w, dw), dw)
            y += 2

            sources = stats.get("statements_sources", [])
            if sources:
                put(scr, y, w, "statement counts", C_TITLE, curses.A_BOLD)
                y += 1
                label_width_src = max(10, dw - 20)
                for entry in sources:
                    if y >= h - 2:
                        break
                    label = entry.get("label") or "statements"
                    label_display = label if len(label) <= label_width_src else label[:label_width_src]
                    put(scr, y, lx, label_display.ljust(label_width_src), C_ACCENT)
                    counts_str = f"{entry['total']:>4} total   {entry['today']:>3} today"
                    put(scr, y, lx + label_width_src + 2, counts_str, C_STAT)
                    y += 1

        # bottom hint
        putc(scr, h - 1, w, "n change name   esc back", C_HINT)
        scr.refresh()

        k = scr.getch()
        if k in (27, ord('q'), ord('Q')):
            return
        elif k == ord('n'):
            new_name = name_prompt(scr, profile.get("name", ""))
            if new_name is not None:
                set_name(new_name)


# ── main ─────────────────────────────────────────────────────────────────────

def run(scr, args):
    theme_name = get_theme()
    if theme_name not in THEMES:
        theme_name = "default"
    init_colors(theme_name)
    curses.curs_set(0)

    # first-launch: prompt for name
    if not profile_exists():
        name = name_prompt(scr)
        if name:
            create_profile(name)
        else:
            create_profile("")

    update_info = get_update_info()

    while True:
        result, theme_name = test(scr, update_info, theme_name)

        if result is None:
            return
        if result == "restart":
            continue
        if result == "stats":
            show_stats(scr)
            continue


def entry():
    parser = argparse.ArgumentParser(
        prog="desire",
        description="desire statement typer — desire statement typing in your terminal",
    )
    parser.add_argument("-t", "--time", type=int, metavar="SEC",
                        help="ignored (no time limit)")
    parser.add_argument("-d", "--diff", type=str, metavar="LEVEL",
                        help="ignored")
    args = parser.parse_args()

    try:
        curses.wrapper(lambda scr: run(scr, args))
    except KeyboardInterrupt:
        pass
