"""
CA Automator — dev server, admin CLI and the night worker.

    python3 run.py init                     create the database and load seed data
    python3 run.py adduser "Name" a@b.com   create a user (prompts for password)
    python3 run.py serve [port]             development server (default 8000)
    python3 run.py worker                   the day-and-night background worker (a persistent process)
    python3 run.py cron-tick                one pass of everything the worker does, then exit (for cPanel/
                                             shared-hosting cron, where a persistent worker process isn't an
                                             option — see README's "Ready to host" section)
    python3 run.py notify                   run the deadline sweep only, once (a narrower cron-tick)
    python3 run.py backup                   write a timestamped database backup
"""
import getpass
import json
import os
import shutil
import subprocess
import sys
import time
from datetime import datetime, timedelta
from wsgiref.simple_server import make_server, WSGIRequestHandler

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import db  # noqa: E402
import notify  # noqa: E402


def cmd_init():
    fresh = db.init_db()
    print(f"Database ready at {db.DB_PATH} ({'created' if fresh else 'already existed, schema refreshed'})")
    print(f"Rate master rows still UNVERIFIED: {db.unverified_count()}")
    print("Next: python3 run.py adduser \"Your Name\" you@firm.com")


def cmd_adduser(argv):
    if len(argv) < 2:
        print('Usage: python3 run.py adduser "Full Name" email@firm.com [role]')
        return
    name, email = argv[0], argv[1]
    role = argv[2] if len(argv) > 2 else "partner"
    pwd = getpass.getpass("Password: ")
    if len(pwd) < 8:
        print("Use at least 8 characters.")
        return
    if pwd != getpass.getpass("Confirm: "):
        print("Those didn't match.")
        return
    db.init_db()
    # The first user in a firm bootstraps it as super admin — otherwise
    # nobody could ever reach the Admin screen to promote themselves.
    # Everyone after that is a normal user by default; this CLI stays
    # available as a break-glass path, but the intended way to add staff
    # going forward is the Admin panel's invite flow.
    is_first = db.query("SELECT COUNT(*) n FROM app_user WHERE firm_id = 1", one=True)["n"] == 0
    uid = db.create_user(1, name, email, pwd, role)
    if is_first:
        db.execute("UPDATE app_user SET is_super_admin = 1 WHERE id = ?", (uid,))
    print(f"Created user #{uid} ({email}, role {role})" + (", promoted to super admin (first user)." if is_first else "."))


class QuietHandler(WSGIRequestHandler):
    def log_message(self, fmt, *args):
        sys.stderr.write("%s  %s\n" % (datetime.now().strftime("%H:%M:%S"), fmt % args))


def cmd_serve(port=8000):
    from app import application
    print(f"CA Automator running at http://localhost:{port}")
    print("Press Ctrl+C to stop. For production use gunicorn (see README).")
    make_server("0.0.0.0", port, application, handler_class=QuietHandler).serve_forever()


# --------------------------------------------------------------- night worker
# sweep_deadlines() itself now lives in notify.py (it also sends email when
# SMTP is configured) so both this worker and POST /api/notifications/sweep
# call the exact same implementation.

def run_jobs(limit=20):
    """Process the job queue — queued report runs and anything else scheduled overnight."""
    done = 0
    jobs = db.query(
        "SELECT * FROM job_queue WHERE status = 'queued' AND run_after <= ? ORDER BY id LIMIT ?",
        (db.now_iso(), limit))
    for j in jobs:
        db.execute("UPDATE job_queue SET status = 'running', attempts = attempts + 1 WHERE id = ?", (j["id"],))
        try:
            if j["job_type"] == "report":
                import engines, reports, app as appmod
                p = json.loads(j["payload"] or "{}")
                result = engines.compute(p["module_key"], p.get("fy") or db.current_fy_label(), p.get("data") or {})
                path, filename = reports.build(p["module_key"], p.get("format", "xlsx"),
                                               result, p.get("meta") or {}, appmod.REPORT_DIR)
                db.execute(
                    "INSERT INTO report (workfile_id, engagement_id, module_key, format, filename,"
                    " storage_path, status, byte_size) VALUES (?,?,?,?,?,?,'ready',?)",
                    (p.get("workfile_id"), p.get("engagement_id"), p["module_key"],
                     p.get("format", "xlsx"), filename, path, os.path.getsize(path)))
            elif j["job_type"] == "backup":
                cmd_backup()
            elif j["job_type"] == "deadline_sweep":
                notify.sweep_deadlines()
            db.execute("UPDATE job_queue SET status = 'done', finished_at = ? WHERE id = ?",
                       (db.now_iso(), j["id"]))
            done += 1
        except Exception as e:
            db.execute("UPDATE job_queue SET status = ?, last_error = ? WHERE id = ?",
                       ("failed" if j["attempts"] >= 2 else "queued", str(e)[:1000], j["id"]))
    return done


def cmd_backup():
    stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    bdir = os.path.join(db.DATA_DIR, "backups")
    os.makedirs(bdir, exist_ok=True)
    if db.DB_ENGINE == "mysql":
        # PRAGMA wal_checkpoint / shutil.copy2(db.DB_PATH, ...) below are
        # SQLite-only — db.DB_PATH is a leftover local file path that isn't
        # the real database once CA_DB_ENGINE=mysql, and PRAGMA isn't valid
        # MySQL syntax at all. Shell out to mysqldump instead, same
        # CA_MYSQL_* config db.py itself reads.
        cfg = db.MYSQL_CONFIG
        dest = os.path.join(bdir, f"ca_automator-{stamp}.sql")
        cmd = ["mysqldump", "-h", cfg["host"], "-P", str(cfg["port"]), "-u", cfg["user"]]
        if cfg["password"]:
            cmd.append(f"-p{cfg['password']}")
        cmd += ["--single-transaction", "--routines", "--events", cfg["database"]]
        with open(dest, "wb") as f:
            subprocess.run(cmd, stdout=f, check=True)
    else:
        dest = os.path.join(bdir, f"ca_automator-{stamp}.db")
        with db.conn() as c:
            c.execute("PRAGMA wal_checkpoint(FULL)")
        shutil.copy2(db.DB_PATH, dest)
    cutoff = datetime.now() - timedelta(days=30)
    for f in os.listdir(bdir):
        p = os.path.join(bdir, f)
        if datetime.fromtimestamp(os.path.getmtime(p)) < cutoff:
            os.remove(p)
    print(f"Backup written: {dest}")
    return dest


def _backed_up_today():
    """Checks the backups directory itself rather than an in-memory flag —
    the right check for cmd_cron_tick, where each invocation is a fresh,
    stateless process with no loop variable to remember the last backup
    date across ticks (unlike cmd_worker's own long-running loop). Without
    this, a cron tick every few minutes would re-run a full mysqldump/copy
    on every single tick for the rest of the day once the hour >= 1 gate
    opens — not "harmless, just redundant" on a shared host with a disk
    quota, so this is checked properly rather than assumed away."""
    bdir = os.path.join(db.DATA_DIR, "backups")
    if not os.path.isdir(bdir):
        return False
    today = datetime.now().date()
    return any(datetime.fromtimestamp(os.path.getmtime(os.path.join(bdir, f))).date() == today
               for f in os.listdir(bdir))


def _worker_tick():
    """One full pass of everything the night worker does — deadline/overdue-
    task/subscription sweeps, the job queue, snapshot pruning, and a once-
    daily backup after 01:00 (gated by _backed_up_today(), so this is safe
    to call from either a long-running loop or a stateless cron tick).
    Shared by cmd_worker (which loops this) and cmd_cron_tick (which runs it
    once and exits, for a host with no persistent-process option — see that
    function's own docstring)."""
    n = notify.sweep_deadlines()
    t = notify.sweep_overdue_tasks()
    s = notify.sweep_subscriptions()
    j = run_jobs()
    db.prune_snapshots(
        retention_days=int((db.query("SELECT value FROM setting WHERE `key`='snapshot_retention_days'",
                                     one=True) or {}).get("value", 120)))
    if datetime.now().hour >= 1 and not _backed_up_today():
        cmd_backup()
    if n or t or s or j:
        print(f"{datetime.now():%Y-%m-%d %H:%M}  reminders {n}, overdue tasks {t}, subscription notices {s}, jobs {j}")


def cmd_worker(interval=300):
    """Runs continuously. Every cycle: sweep deadlines, run jobs, prune, back up nightly."""
    db.init_db()
    print(f"Night worker started (cycle {interval}s). Ctrl+C to stop.")
    while True:
        try:
            _worker_tick()
        except Exception as e:
            print("worker error:", e)
        time.sleep(interval)


def cmd_cron_tick():
    """One pass of everything cmd_worker's loop does, then exit — the right
    shape for a host with no persistent-background-process option (cPanel/
    shared hosting via Passenger: the app only runs per-request, there is no
    daemon to hold a `while True` loop). Point a cron job at this every few
    minutes instead of running `worker` — same substitution the billing
    system's Laravel counterpart makes for its queue worker (`queue:work
    --stop-when-empty` on a cron tick rather than a persistent `queue:work`
    daemon)."""
    db.init_db()
    _worker_tick()


if __name__ == "__main__":
    cmd = sys.argv[1] if len(sys.argv) > 1 else "serve"
    if cmd == "init":
        cmd_init()
    elif cmd == "adduser":
        cmd_adduser(sys.argv[2:])
    elif cmd == "serve":
        cmd_serve(int(sys.argv[2]) if len(sys.argv) > 2 else 8000)
    elif cmd == "worker":
        cmd_worker(int(sys.argv[2]) if len(sys.argv) > 2 else 300)
    elif cmd == "cron-tick":
        cmd_cron_tick()
    elif cmd == "notify":
        db.init_db()
        print(f"Reminders raised: {notify.sweep_deadlines()}")
    elif cmd == "backup":
        cmd_backup()
    else:
        print(__doc__)
