"""Berberistan - backend (Flask + SQLite).

Çalıştırma:   python app.py
Kullanıcı:    python app.py adduser <kullanici_adi> "<Ad>" <boss|staff>
Deneme verisi: python app.py demo
"""
import os, re, sys, time, random, sqlite3, secrets, getpass, tempfile, threading, shutil
from datetime import datetime, date, timedelta
from functools import wraps
from flask import (Flask, g, jsonify, request, session, send_from_directory,
                   send_file, after_this_request)
from werkzeug.security import generate_password_hash, check_password_hash

BASE = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.environ.get("SALON_DB", os.path.join(BASE, "salon.db"))
BACKUP_DIR = os.environ.get("SALON_BACKUPS", os.path.join(BASE, "backups"))
KEY_PATH = os.path.join(BASE, ".secret_key")

def secret_key():
    k = os.environ.get("SECRET_KEY")
    if k:
        return k
    if not os.path.exists(KEY_PATH):
        with open(KEY_PATH, "w") as f:
            f.write(secrets.token_hex(32))
        os.chmod(KEY_PATH, 0o600)
    with open(KEY_PATH) as f:
        return f.read().strip()

app = Flask(__name__, static_folder="static", static_url_path="")
app.secret_key = secret_key()
app.config.update(
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="Lax",
    SESSION_COOKIE_SECURE=os.environ.get("HTTPS") == "1",  # HTTPS ile yayınlarken HTTPS=1 yapın
    PERMANENT_SESSION_LIFETIME=60 * 60 * 12,
    MAX_CONTENT_LENGTH=64 * 1024,
)

SCHEMA = """
CREATE TABLE IF NOT EXISTS users(
  id INTEGER PRIMARY KEY, username TEXT UNIQUE NOT NULL, name TEXT NOT NULL,
  role TEXT NOT NULL CHECK(role IN ('boss','staff')), pw_hash TEXT NOT NULL,
  active INTEGER NOT NULL DEFAULT 1);
CREATE TABLE IF NOT EXISTS appointments(
  id INTEGER PRIMARY KEY, staff_id INTEGER NOT NULL REFERENCES users(id),
  date TEXT NOT NULL, time TEXT NOT NULL, dur INTEGER NOT NULL,
  customer TEXT NOT NULL, service TEXT NOT NULL, price INTEGER NOT NULL,
  pay TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'bekliyor',
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);
CREATE INDEX IF NOT EXISTS idx_appt ON appointments(staff_id, date);
"""
PAY = ("Nakit", "Kart", "Havale")
STATUS = ("bekliyor", "geldi", "gelmedi", "iptal")
OPEN_MIN, CLOSE_MIN = 9 * 60, 19 * 60

def init_db():
    c = sqlite3.connect(DB_PATH)
    c.executescript(SCHEMA)
    if "phone" not in [r[1] for r in c.execute("PRAGMA table_info(appointments)")]:
        c.execute("ALTER TABLE appointments ADD COLUMN phone TEXT NOT NULL DEFAULT ''")
    c.commit()
    c.close()

def db():
    if "db" not in g:
        g.db = sqlite3.connect(DB_PATH, timeout=10)
        g.db.row_factory = sqlite3.Row
        g.db.execute("PRAGMA foreign_keys=ON")
    return g.db

@app.teardown_appcontext
def close_db(_):
    d = g.pop("db", None)
    if d:
        d.close()

@app.after_request
def headers(r):
    r.headers["X-Content-Type-Options"] = "nosniff"
    r.headers["Referrer-Policy"] = "same-origin"
    if request.path.startswith("/api/"):
        r.headers["Cache-Control"] = "no-store"
    return r

# ---------- yardımcılar ----------
_TR = str.maketrans("İŞşĞğÜüÖöÇçı", "issgguuoocci")
def norm(s):
    return str(s).strip().translate(_TR).lower()

def mins(t):
    h, m = t.split(":")
    return int(h) * 60 + int(m)

def public(u):
    return {"id": u["username"], "name": u["name"], "role": u["role"]}

def appt_json(r):
    return {k: r[k] for k in ("id", "date", "time", "dur", "customer", "service", "price", "pay", "status", "phone")} | {"staff": r["username"]}

Q = "SELECT a.*, u.username FROM appointments a JOIN users u ON u.id=a.staff_id"

def err(msg, code):
    return jsonify(error=msg), code

def auth(boss=False):
    def deco(fn):
        @wraps(fn)
        def w(*a, **k):
            uid = session.get("uid")
            u = db().execute("SELECT * FROM users WHERE id=? AND active=1", (uid,)).fetchone() if uid else None
            if not u:
                session.clear()
                return err("Giriş yapmanız gerekiyor.", 401)
            if boss and u["role"] != "boss":
                return err("Bu işlem için patron yetkisi gerekir.", 403)
            g.user = u
            return fn(*a, **k)
        return w
    return deco

def find_conflict(staff_id, day, start, dur, exclude=None):
    rows = db().execute("SELECT * FROM appointments WHERE staff_id=? AND date=? AND status!='iptal'", (staff_id, day))
    for r in rows:
        if r["id"] == exclude:
            continue
        s = mins(r["time"])
        if s < start + dur and start < s + r["dur"]:
            return r
    return None

# ---------- giriş ----------
FAILS = {}
def too_many(key):
    now = time.time()
    FAILS[key] = [t for t in FAILS.get(key, []) if now - t < 300]
    return len(FAILS[key]) >= 5

@app.post("/api/login")
def login():
    d = request.get_json(silent=True) or {}
    un, pw = norm(d.get("username", "")), str(d.get("password", ""))
    key = (request.remote_addr, un)
    if too_many(key):
        return err("Çok fazla hatalı deneme. 5 dakika sonra tekrar deneyin.", 429)
    u = db().execute("SELECT * FROM users WHERE username=? AND active=1", (un,)).fetchone()
    if not u or not check_password_hash(u["pw_hash"], pw):
        FAILS.setdefault(key, []).append(time.time())
        return err("Kullanıcı adı veya şifre hatalı. Tekrar deneyin.", 401)
    session.clear()
    session["uid"] = u["id"]
    session.permanent = True
    return jsonify(user=public(u))

@app.post("/api/logout")
def logout():
    session.clear()
    return jsonify(ok=True)

@app.get("/api/me")
def me():
    uid = session.get("uid")
    u = db().execute("SELECT * FROM users WHERE id=? AND active=1", (uid,)).fetchone() if uid else None
    return jsonify(user=public(u) if u else None)

@app.get("/api/users")
@auth()
def users():
    rows = db().execute("SELECT * FROM users WHERE active=1 ORDER BY role DESC, name").fetchall()
    return jsonify([public(u) for u in rows])

# ---------- randevular ----------
@app.get("/api/appointments")
@auth()
def list_appts():
    sql, args = Q + " WHERE 1=1", []
    if g.user["role"] != "boss":
        sql += " AND a.staff_id=?"
        args.append(g.user["id"])
    if request.args.get("from"):
        sql += " AND a.date>=?"
        args.append(request.args["from"])
    if request.args.get("to"):
        sql += " AND a.date<=?"
        args.append(request.args["to"])
    rows = db().execute(sql + " ORDER BY a.date, a.time", args)
    return jsonify([appt_json(r) for r in rows])

@app.post("/api/appointments")
@auth()
def create_appt():
    d = request.get_json(silent=True) or {}
    try:
        customer = str(d["customer"]).strip()
        service = (str(d.get("service", "")).strip() or "Hizmet")[:60]
        day, tm, pay = str(d["date"]), str(d["time"]), d["pay"]
        dur, price = int(d["dur"]), int(d["price"])
        datetime.strptime(day, "%Y-%m-%d")
        if not re.fullmatch(r"\d{2}:\d{2}", tm):
            raise ValueError
        start = mins(tm)
    except (KeyError, ValueError, TypeError):
        return err("Eksik veya hatalı bilgi var.", 400)
    if not 1 <= len(customer) <= 80 or pay not in PAY or not 5 <= dur <= 480 or not 0 <= price <= 1_000_000:
        return err("Müşteri adı, süre, ücret veya ödeme türü geçersiz.", 400)
    if start < OPEN_MIN or start + dur > CLOSE_MIN:
        return err("Randevu 09:00 ile 19:00 arasında olmalı. Saati veya süreyi değiştirin.", 400)
    phone = re.sub(r"[^\d+]", "", str(d.get("phone", "")))[:16]
    if phone and not 10 <= len(phone.lstrip("+")) <= 15:
        return err("Telefon numarası geçersiz.", 400)
    if g.user["role"] == "boss":
        st = db().execute("SELECT * FROM users WHERE username=? AND role='staff' AND active=1", (norm(d.get("staff", "")),)).fetchone()
        if not st:
            return err("Kuaför bulunamadı.", 400)
    else:
        st = g.user
    con = db()
    con.execute("BEGIN IMMEDIATE")
    try:
        c = find_conflict(st["id"], day, start, dur)
        if c:
            con.rollback()
            return err(f"{st['name']} için {c['time']} saatinde {c['customer']} randevusu var ({c['dur']} dk). Başka bir saat seçin.", 409)
        cur = con.execute("INSERT INTO appointments(staff_id,date,time,dur,customer,service,price,pay,phone) VALUES(?,?,?,?,?,?,?,?,?)",
                          (st["id"], day, f"{start // 60:02d}:{start % 60:02d}", dur, customer, service, price, pay, phone))
        con.commit()
    except Exception:
        con.rollback()
        raise
    return jsonify(appt_json(con.execute(Q + " WHERE a.id=?", (cur.lastrowid,)).fetchone())), 201

@app.patch("/api/appointments/<int:aid>")
@auth()
def set_status(aid):
    status = (request.get_json(silent=True) or {}).get("status")
    if status not in STATUS:
        return err("Geçersiz durum.", 400)
    row = db().execute(Q + " WHERE a.id=?", (aid,)).fetchone()
    if not row or (g.user["role"] != "boss" and row["staff_id"] != g.user["id"]):
        return err("Randevu bulunamadı.", 404)
    if row["status"] == "iptal" and status != "iptal":
        c = find_conflict(row["staff_id"], row["date"], mins(row["time"]), row["dur"], exclude=aid)
        if c:
            return err(f"Bu randevu geri alınamaz, {c['time']} saatinde {c['customer']} randevusu var.", 409)
    db().execute("UPDATE appointments SET status=? WHERE id=?", (status, aid))
    db().commit()
    return jsonify(appt_json(db().execute(Q + " WHERE a.id=?", (aid,)).fetchone()))

# ---------- kuaför yönetimi (patron) ----------
@app.get("/api/staff")
@auth(boss=True)
def list_staff():
    rows = db().execute("SELECT * FROM users WHERE role='staff' ORDER BY active DESC, name").fetchall()
    return jsonify([public(u) | {"active": bool(u["active"])} for u in rows])

@app.post("/api/staff")
@auth(boss=True)
def add_staff():
    d = request.get_json(silent=True) or {}
    un, name, pw = norm(d.get("username", "")), str(d.get("name", "")).strip(), str(d.get("password", ""))
    if not re.fullmatch(r"[a-z0-9_.-]{3,30}", un) or not name or len(pw) < 6:
        return err("Kullanıcı adı 3-30 harf/rakam, şifre en az 6 karakter olmalı.", 400)
    try:
        db().execute("INSERT INTO users(username,name,role,pw_hash) VALUES(?,?,'staff',?)", (un, name[:60], generate_password_hash(pw)))
        db().commit()
    except sqlite3.IntegrityError:
        return err("Bu kullanıcı adı zaten var.", 409)
    return jsonify(user={"id": un, "name": name, "role": "staff"}), 201

@app.patch("/api/staff/<username>")
@auth(boss=True)
def edit_staff(username):
    d = request.get_json(silent=True) or {}
    u = db().execute("SELECT * FROM users WHERE username=? AND role='staff'", (norm(username),)).fetchone()
    if not u:
        return err("Kuaför bulunamadı.", 404)
    if "active" in d:
        db().execute("UPDATE users SET active=? WHERE id=?", (1 if d["active"] else 0, u["id"]))
    if d.get("password"):
        if len(str(d["password"])) < 6:
            return err("Şifre en az 6 karakter olmalı.", 400)
        db().execute("UPDATE users SET pw_hash=? WHERE id=?", (generate_password_hash(str(d["password"])), u["id"]))
    db().commit()
    return jsonify(ok=True)

# ---------- yedek ----------
def snapshot(path):
    src = sqlite3.connect(DB_PATH)
    dst = sqlite3.connect(path)
    src.backup(dst)
    dst.close()
    src.close()

@app.get("/api/backup")
@auth(boss=True)
def backup():
    fd, path = tempfile.mkstemp(suffix=".db")
    os.close(fd)
    snapshot(path)
    @after_this_request
    def rm(r):
        try:
            os.remove(path)
        except OSError:
            pass
        return r
    return send_file(path, as_attachment=True, download_name=f"berberistan-yedek-{datetime.now():%Y%m%d-%H%M}.db")

def auto_backup():
    """Her 24 saatte bir backups/ klasörüne kopya alır, son 14 kopyayı tutar."""
    while True:
        try:
            os.makedirs(BACKUP_DIR, exist_ok=True)
            snapshot(os.path.join(BACKUP_DIR, f"salon-{datetime.now():%Y%m%d-%H%M}.db"))
            files = sorted(os.listdir(BACKUP_DIR))
            for f in files[:-14]:
                os.remove(os.path.join(BACKUP_DIR, f))
        except Exception as e:
            print("Otomatik yedek hatası:", e)
        time.sleep(24 * 3600)

@app.get("/")
def index():
    return send_from_directory(app.static_folder, "index.html")

# ---------- komut satırı ----------
def add_user_cli(username, name, role):
    if role not in ("boss", "staff"):
        sys.exit("Rol boss veya staff olmalı.")
    pw = getpass.getpass("Şifre (en az 6 karakter): ")
    if len(pw) < 6 or pw != getpass.getpass("Şifre tekrar: "):
        sys.exit("Şifreler uyuşmuyor ya da çok kısa.")
    c = sqlite3.connect(DB_PATH)
    c.execute("INSERT INTO users(username,name,role,pw_hash) VALUES(?,?,?,?)", (norm(username), name, role, generate_password_hash(pw)))
    c.commit()
    print("Kullanıcı eklendi:", norm(username))

def demo():
    c = sqlite3.connect(DB_PATH)
    for un, n, r in [("patron", "Patron", "boss"), ("ayse", "Ayşe", "staff"), ("mehmet", "Mehmet", "staff"), ("elif", "Elif", "staff")]:
        c.execute("INSERT OR IGNORE INTO users(username,name,role,pw_hash) VALUES(?,?,?,?)", (un, n, r, generate_password_hash("1234")))
    if c.execute("SELECT COUNT(*) FROM appointments").fetchone()[0] == 0:
        rnd = random.Random(7)
        serv = [("Saç kesimi", 400, 45), ("Fön", 250, 30), ("Boya", 1500, 120), ("Röfle", 2200, 150), ("Sakal", 200, 20), ("Manikür", 350, 45)]
        names = ["Zeynep K.", "Can Y.", "Selin A.", "Burak T.", "Merve D.", "Emre S."]
        ids = [r[0] for r in c.execute("SELECT id FROM users WHERE role='staff'")]
        for off in range(-35, 3):
            day = date.today() + timedelta(days=off)
            if day.weekday() == 6:
                continue
            for sid in ids:
                t = 540 + rnd.randrange(3) * 30
                for _ in range(rnd.randint(3, 5)):
                    s = rnd.choice(serv)
                    if t + s[2] > CLOSE_MIN:
                        break
                    nm = rnd.choice(names)
                    r = rnd.random()
                    st = "bekliyor" if off >= 0 else ("gelmedi" if r < .08 else "iptal" if r < .15 else "geldi")
                    c.execute("INSERT INTO appointments(staff_id,date,time,dur,customer,service,price,pay,status,phone) VALUES(?,?,?,?,?,?,?,?,?,?)",
                              (sid, day.isoformat(), f"{t // 60:02d}:{t % 60:02d}", s[2], nm, s[0], s[1], rnd.choice(PAY), st, "0532 000 %02d %02d" % (names.index(nm), names.index(nm) + 10)))
                    t += s[2] + rnd.randrange(3) * 30
    c.commit()
    print("Deneme verisi hazır. Giriş: patron / ayse / mehmet / elif, şifre 1234")

init_db()
if os.environ.get("SALON_AUTOBACKUP", "1") == "1" and os.environ.get("WERKZEUG_RUN_MAIN") != "false":
    threading.Thread(target=auto_backup, daemon=True).start()

if __name__ == "__main__":
    if len(sys.argv) == 5 and sys.argv[1] == "adduser":
        add_user_cli(*sys.argv[2:5])
    elif len(sys.argv) == 2 and sys.argv[1] == "demo":
        demo()
    else:
        app.run(host=os.environ.get("HOST", "127.0.0.1"), port=int(os.environ.get("PORT", 5000)))
