# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.

# %% auto 0
__all__ = ['app', 'rt', 'AdminConfig', 'create_admin', 'tbl_name', 'mock_req', 'url', 'get_col_types', 'get_cols',
           'get_text_cols', 'get_pk_col', 'is_dangerous_query', 'LabeledInput', 'HeaderBar', 'Layout', 'NewRowBtn',
           'EditableCell', 'EditCellInput', 'TableRows', 'NewRowModal', 'SqlConsole', 'DbStats', 'TableStats',
           'LoginPage', 'TableCard', 'PageBtns', 'SearchForm', 'get', 'post', 'auth_check', 'DangerousQueryModal',
           'put', 'delete', 'execute_and_render_sql', 'search_rows', 'paginate']

# %% ../nbs/00_core.ipynb 5
from fasthtml.common import *
from fasthtml.jupyter import * 
from fhdaisy import *

from fastcore.utils import *

from fh_heroicons import Heroicon 

from fasthtml.common import Input as Input_

from pathlib import Path

from functools import cached_property

import re



# %% ../nbs/00_core.ipynb 7
app = FastHTML(hdrs=(daisy_hdrs,), session_cookie="fastadmin_")

rt = app.route

# %% ../nbs/00_core.ipynb 14
@dataclass
class AdminConfig:
    db_path: str | Path
    password: str = "admin123"
    
    @cached_property
    def db(self): return database(self.db_path)

# %% ../nbs/00_core.ipynb 16
def create_admin(config):
    app.state.cfg = config
    return app

# %% ../nbs/00_core.ipynb 19
def tbl_name(t): return t.name.strip('"') if hasattr(t, 'name') else str(t).strip('"')

# %% ../nbs/00_core.ipynb 20
def mock_req(root_path=""):
    from starlette.requests import Request
    return Request({"type": "http", "root_path": root_path, "headers": []})

_mock_req = mock_req()

def url(req, path): return f"{req.scope.get('root_path', '')}{path}"

# %% ../nbs/00_core.ipynb 21
def get_col_types(db, t):
    name = tbl_name(t)
    return {r[1]: r[2] for r in db.execute(f'PRAGMA table_info("{name}")').fetchall()}

# %% ../nbs/00_core.ipynb 22
def get_cols(db, t):
    name = tbl_name(t)
    return [r[1] for r in db.execute(f'PRAGMA table_info("{name}")').fetchall()]

# %% ../nbs/00_core.ipynb 23
def get_text_cols(db, t):
    return [name for name, typ in get_col_types(db, t).items() if 'TEXT' in typ.upper() or 'CHAR' in typ.upper()]

# %% ../nbs/00_core.ipynb 24
def get_pk_col(db, t):
    name = tbl_name(t)
    for r in db.execute(f'PRAGMA table_info("{name}")').fetchall():
        if r[5]: return r[1]  # r[5] is pk flag, r[1] is column name
    return None

# %% ../nbs/00_core.ipynb 26
def is_dangerous_query(query:str):
    return bool(re.search(r"drop table|delete|truncate|update|alter table", query.lower()))

# %% ../nbs/00_core.ipynb 30
mk_compfn("btn", "A", "LinkButton")

# %% ../nbs/00_core.ipynb 32
def LabeledInput(icon_name, **kwargs):
    return Label(
        Span(Heroicon(icon_name), cls="label"),
        Input_(cls="w-full", **kwargs),
        cls="input w-full"
    )

# %% ../nbs/00_core.ipynb 34
def HeaderBar(*,req=_mock_req, logged_in=False):
    logout_btn = Form(method="post", action=url(req, "/logout"))(
        Btn(Heroicon("arrow-right-on-rectangle"), "Logout", cls="-ghost -sm gap-1")) if logged_in else None
    return Div(cls="navbar bg-base-200 mb-6")(
        Div(cls="flex-1")(LinkButton("fhadmin", href=url(req, "/tables"), cls="-ghost text-xl")),
        Div(cls="flex-none")(logout_btn))

# %% ../nbs/00_core.ipynb 36
def Layout(*children, title="Admin", req=_mock_req, logged_in=False):
    return (Title(title), 
            HeaderBar(req=req, logged_in=logged_in),
            Main(cls="max-w-[1800px] mx-auto px-6 py-8")(*children))

# %% ../nbs/00_core.ipynb 37
def NewRowBtn(tbl, *, req=_mock_req): return Btn(Heroicon("plus"), "New Record", cls="-primary -sm flex items-center gap-2", hx_get=url(req, f"/tables/{tbl}/new"), hx_target="body", hx_swap="beforeend")

# %% ../nbs/00_core.ipynb 39
def EditableCell(tbl, pk, col, val, *, req=_mock_req):
    return Td(str(val), hx_get=url(req, f"/tables/{tbl}/{pk}/edit/{col}"), hx_swap="outerHTML", cls="cursor-pointer hover:bg-base-200")

# %% ../nbs/00_core.ipynb 40
def EditCellInput(tbl, pk, col, val, *, req=_mock_req):
    return Td(Input(value=val, name=col, cls="-bordered -sm w-full", autofocus=True,
                     hx_put=url(req, f"/tables/{tbl}/{pk}/edit/{col}"), hx_target="closest td", hx_trigger="blur, keyup[key=='Enter']", hx_swap="outerHTML"))

# %% ../nbs/00_core.ipynb 41
def TableRows(db, rows, cols, tbl, *, req=_mock_req):
    pk_col = get_pk_col(db, tbl)
    return [Tr(*[EditableCell(tbl, r[pk_col], c, r.get(c, ''), req=req) for c in cols],
               Td(Btn(Heroicon("trash"), cls="-ghost -xs", hx_delete=url(req, f"/tables/{tbl}/{r[pk_col]}"), hx_target="closest tr", hx_swap="outerHTML")))
            for r in rows]

# %% ../nbs/00_core.ipynb 42
def NewRowModal(tbl, cols, *, req=_mock_req):
    inputs = [Div(cls="form-control")(Label(c, cls="label"), Input_(name=c, cls="input input-bordered w-full")) for c in cols[1:]]
    return Dialog(id="new-row-modal", cls="modal modal-open")(
        Div(cls="modal-box")(
            H3(f"New {tbl} record", cls="font-bold text-lg mb-4"),
            Form(method="post", action=url(req, f"/tables/{tbl}/new"), hx_post=url(req, f"/tables/{tbl}/new"), hx_target="#table-content", hx_swap="outerHTML")(*inputs,
                Div(cls="modal-action")(
                    Button("Cancel", cls="btn", type="button", onclick="document.getElementById('new-row-modal').remove()"),
                    Button("Create", cls="btn btn-primary", type="submit")))))

# %% ../nbs/00_core.ipynb 43
def SqlConsole(tbl, *, req=_mock_req):
    return Div(
        H3("SQL Console", cls="font-bold mb-2"),
        Form(hx_post=url(req, f"/tables/{tbl}/sql"), hx_target="#sql-results", hx_swap="innerHTML", cls="flex flex-col gap-2")(
            Textarea(name="sql", placeholder=f"SELECT * FROM {tbl} LIMIT 10", cls="-bordered w-full font-mono", rows=3),
            Div(cls="flex gap-2")(
                Btn("Run", cls="-sm -secondary"),
                Btn("Reset", type="button", cls="-sm -ghost",
                       onclick="this.form.sql.value=''; document.getElementById('sql-results').innerHTML='';"))),
        Div(id="sql-results", cls="mt-4")),
 

# %% ../nbs/00_core.ipynb 44
def DbStats(cfg):
    path = Path(cfg.db_path)
    size_mb = path.stat().st_size / (1024 * 1024)
    return Card(cls="bg-base-200 p-4 mb-6")(
        Div(cls="flex gap-8")(
            Div(Span("Database: ", cls="font-bold"), Span(path.name)),
            Div(Span("Size: ", cls="font-bold"), Span(f"{size_mb:.2f} MB")),
        )
    )

# %% ../nbs/00_core.ipynb 46
def TableStats(db, tbl):
    t = db.t[tbl]
    cols = get_cols(db, t)
    row_count = db.execute(f"SELECT COUNT(*) FROM \"{tbl}\"").fetchone()[0]
    col_types = get_col_types(db, t)
    
    return Card(cls="bg-base-200 p-4 mb-4")(
        Div(cls="flex gap-8")(
            Div(Span("Rows: ", cls="font-bold"), Span(f"{row_count:,}")),
            Div(Span("Columns: ", cls="font-bold"), Span(str(len(cols)))),
        ),
        Div(cls="text-sm text-gray-500 mt-2")(
            ", ".join(f"{c} ({col_types[c]})" for c in cols)
        )
    )

# %% ../nbs/00_core.ipynb 48
def LoginPage(*, req=_mock_req):
    return Div(cls="min-h-screen flex items-center justify-center")(
        Card(cls="w-full max-w-sm shadow-xl")(
            CardBody(
                H2("Welcome Back", cls="card-title justify-center mb-4"),
                Form(method="post", action=url(req, "/login"))(
                    LabeledInput("lock-closed", type="password", name="password", placeholder="Password", required=True),
                    Btn("Login", cls="-primary w-full mt-4")))))

# %% ../nbs/00_core.ipynb 50
def TableCard(name, row_count, col_count, *, req=_mock_req):
    return A(href=url(req, f"/tables/{name}"))(
        Card(cls="bg-base-200 hover:bg-base-300 transition-colors cursor-pointer")(
            CardBody(
                H3(name, cls="card-title"),
                Div(cls="flex gap-4 text-sm opacity-70")(
                    Span(f"{row_count:,} rows"),
                    Span(f"{col_count} columns")
                )
            )
        )
    )

# %% ../nbs/00_core.ipynb 52
def PageBtns(tbl, page, total_pages, q="", *, req=_mock_req):
    if total_pages <= 7:
        pages = list(range(1, total_pages + 1))
    else:
        pages = [1, 2]
        if page > 4: pages.append(("...", 1))
        pages.extend(i for i in range(max(3, page-1), min(total_pages-1, page+2)))
        if page < total_pages - 3: pages.append(("...", 2))
        pages.extend([total_pages-1, total_pages])
        pages = list(dict.fromkeys(pages))  # remove duplicates, preserve order
    return [Span("...", cls="px-2") if isinstance(p, tuple) else LinkButton(str(p), href=url(req, f"/tables/{tbl}?page={p}&q={q}"), cls=f"-sm {'-active' if p == page else ''}") for p in pages]

# %% ../nbs/00_core.ipynb 54
def SearchForm(tbl, q="", *, req=_mock_req):
    return Form(cls="flex gap-2 items-center")(
        Input(name="q", value=q, placeholder="Search...", cls="-bordered -sm w-64",
               hx_get=url(req, f"/tables/{tbl}"), hx_trigger="input changed delay:300ms", hx_target="#table-content", hx_swap="innerHTML", hx_push_url="true"),
        LinkButton("Reset", href=url(req, f"/tables/{tbl}"), cls="-sm -ghost"))

# %% ../nbs/00_core.ipynb 57
@rt("/login")
def get(req): return Layout(LoginPage(req=req), title="Admin Login", req=req, logged_in=False)

@rt("/login")
def post(req, password: str, sess):
    if password != app.state.cfg.password: return Layout(LoginPage(req=req), Alert("Invalid password", cls="alert-error mt-4"), title="Admin Login", req=req, logged_in=False)
    sess['auth'] = True
    return Redirect(url(req, "/tables"))

# %% ../nbs/00_core.ipynb 58
@rt("/logout")
def post(req, sess):
    sess.pop('auth', None)
    return Redirect(url(req, "/login"))

# %% ../nbs/00_core.ipynb 59
def auth_check(req, sess):
    if not sess.get('auth'): return Redirect(url(req, "/login"))

# %% ../nbs/00_core.ipynb 60
def DangerousQueryModal(tbl, sql, *, req=_mock_req):
    return Modal(id="danger-modal", cls="-open")(
        ModalBox(
            H3("⚠️ Dangerous Query Detected", cls="font-bold text-lg text-warning mb-4"),
            P("This query could modify or delete data:", cls="mb-2"),
            Pre(sql, cls="bg-base-200 p-3 rounded text-sm mb-4"),
            P("Are you sure you want to execute it?", cls="font-semibold"),
            ModalAction(
                Btn("Cancel", type="button", 
                       onclick="document.getElementById('danger-modal').remove()"),
                Form(hx_post=url(req, f"/tables/{tbl}/sql/confirm"), 
                     hx_target="#sql-results", hx_swap="innerHTML")(
                    Input(type="hidden", name="sql", value=sql),
                    Btn("Confirm & Execute", cls="-error", type="submit")
                )
            )
        )
    )

# %% ../nbs/00_core.ipynb 63
def _stats(t):
    table, _, stats = t
    return table, int(stats.split()[0])

# %% ../nbs/00_core.ipynb 65
@rt("/tables")
def get(req, sess):
    if redir := auth_check(req, sess): return redir
    db = app.state.cfg.db
    try:
        row_counts = dict(L(db.execute("SELECT * FROM sqlite_stat1").fetchall()).map(_stats))
    except:
        row_counts = {tbl_name(t): db.execute(f'SELECT COUNT(*) FROM "{tbl_name(t)}"').fetchone()[0] for t in db.t}
    cards = [TableCard(tbl_name(t), row_counts.get(tbl_name(t), 0), len(get_cols(db, t)), req=req) for t in db.t]
    return Layout(
        DbStats(app.state.cfg),
        H1("Tables", cls="text-2xl font-bold mb-6"),
        Div(cls="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4")(*cards),
        title="Tables", req=req, logged_in=True
    )

# %% ../nbs/00_core.ipynb 67
@rt("/tables/{tbl}/{pk}/edit/{col}")
def get(req, sess, tbl: str, pk: str, col: str):
    if redir := auth_check(req, sess): return redir
    db = app.state.cfg.db
    row = db.t[tbl][pk]
    return EditCellInput(tbl, pk, col, row.get(col, ''), req=req)

# %% ../nbs/00_core.ipynb 68
@rt("/tables/{tbl}/new")
def get(req, sess, tbl: str):
    if redir := auth_check(req, sess): return redir
    db = app.state.cfg.db
    return NewRowModal(tbl, get_cols(db, db.t[tbl]), req=req)

# %% ../nbs/00_core.ipynb 69
@rt("/tables/{tbl}/new")
async def post(req, sess, tbl: str):
    if redir := auth_check(req, sess): return redir
    db = app.state.cfg.db
    form = await req.form()
    data = {k: v for k, v in form.items() if v}
    db.t[tbl].insert(**data)
    return Redirect(url(req, f"/tables/{tbl}"))

@rt("/tables/{tbl}/{pk}/edit/{col}")
async def put(req, sess, tbl: str, pk: str, col: str):
    if redir := auth_check(req, sess): return redir
    db = app.state.cfg.db
    form = await req.form()
    val = form.get(col, '')
    t = db.t[tbl]
    pk_col = get_cols(db, t)[0]
    db.execute(f"UPDATE {tbl} SET {col} = ? WHERE {pk_col} = ?", [val, pk])
    return EditableCell(tbl, pk, col, val, req=req)

# %% ../nbs/00_core.ipynb 71
@rt("/tables/{tbl}/{pk}")
def delete(req, sess, tbl: str, pk: str):
    if not sess.get('auth'): return Redirect(url(req, "/login"))
    db = app.state.cfg.db
    t = db.t[tbl]
    t.delete(pk)
    return ""

# %% ../nbs/00_core.ipynb 73
def execute_and_render_sql(db, sql):
    try:
        cursor = db.execute(sql)
        cols = [desc[0] for desc in cursor.description]        
        result = cursor.fetchall()
        if not result: return Div("Query executed successfully. No results.", cls="text-success")
        header = Tr(*[Th(c) for c in cols])
        body = [Tr(*[Td(str(v)) for v in row]) for row in result[:100]]
        return Div(cls="overflow-x-auto")(
            Table(cls="-zebra table-sm")(Thead(header), Tbody(*body)),
            P(f"Showing {len(body)} of {len(result)} rows", cls="text-sm text-gray-500 mt-2") if len(result) > 100 else None
        )
    except Exception as e: 
        return Div(f"Error: {e}", cls="text-error")

# %% ../nbs/00_core.ipynb 74
@rt("/tables/{tbl}/sql")
def post(req, sess, tbl: str, sql: str):
    if redir := auth_check(req, sess): return redir
    db = app.state.cfg.db
    sql = sql.strip()
    if not sql: return Div("No query provided", cls="text-error")
    
    if is_dangerous_query(sql):
        return DangerousQueryModal(tbl, sql, req=req)
    
    return execute_and_render_sql(db, sql)

@rt("/tables/{tbl}/sql/confirm")
def post(req, sess, tbl: str, sql: str):
    if redir := auth_check(req, sess): return redir
    db = app.state.cfg.db
    return execute_and_render_sql(db, sql)

# %% ../nbs/00_core.ipynb 75
def search_rows(db, tbl, cols, text_cols, q):
    if not (q and text_cols): return db.t[tbl]()
    where = " OR ".join([f"{c} LIKE ?" for c in text_cols])
    rows = db.execute(f"SELECT * FROM {tbl} WHERE {where}", [f"%{q}%" for _ in text_cols]).fetchall()
    return [dict(zip(cols, r)) for r in rows]

# %% ../nbs/00_core.ipynb 76
def paginate(rows, page, per_page=25):
    total = len(rows)
    total_pages = max(1, (total + per_page - 1) // per_page)
    return rows[(page-1)*per_page : page*per_page], total_pages

# %% ../nbs/00_core.ipynb 77
@rt("/tables/{tbl}")
def get(req, sess, tbl: str, page: int = 1, q: str = ""):
    if redir := auth_check(req, sess): return redir
    db = app.state.cfg.db
    t = db.t[tbl]
    cols = get_cols(db, t)
    all_rows = search_rows(db, tbl, cols, get_text_cols(db, t), q)
    rows, total_pages = paginate(all_rows, page)
    
    header = Tr(*[Th(c) for c in cols], Th("Actions"))
    
    table_content = Div(
        Table(cls="table table-zebra")(Thead(header), Tbody(*TableRows(db, rows, cols, tbl, req=req))),
        Div(cls="flex gap-2 mt-4 justify-center")(*PageBtns(tbl, page, total_pages, q, req=req)))
    
    # If HTMX request, return just the table content
    if req.headers.get("hx-request"):
        return table_content

    left = Div(cls="lg:col-span-2")(Div(cls="overflow-x-auto", id="table-content")(table_content))

    right = Div(cls="lg:col-span-1 flex flex-col gap-4")(
        Div(cls="card bg-base-200 p-4")(H3("Search", cls="font-bold mb-2"), SearchForm(tbl, q, req=req)),
        Div(cls="card bg-base-200 p-4")(SqlConsole(tbl, req=req)))

    return Layout(
        Div(cls="flex items-center justify-between mb-4")(
            LinkButton(Heroicon("arrow-left", cls="size-4"), "Back", href=url(req, "/tables"), cls="-ghost -sm gap-1"), H1(tbl, cls="text-2xl font-bold inline"),
            NewRowBtn(tbl, req=req)),
        TableStats(db, tbl),
        Div(cls="grid grid-cols-1 lg:grid-cols-3 gap-6")(left, right),
        title=tbl, req=req, logged_in=True)
