from flask import Flask, request, jsonify, send_file, render_template, session, redirect, url_for
import csv, io, re, json, os, requests, secrets
from datetime import datetime, date
from functools import wraps
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", secrets.token_hex(32))

BASE_DIR = os.path.dirname(__file__)
DATA_FILE = os.path.join(BASE_DIR, "data", "domains.json")
USERS_FILE = os.path.join(BASE_DIR, "data", "users.json")


# ─── JSON Persistence ───
def load_json_file(path, default):
    if os.path.exists(path):
        try:
            with open(path, "r", encoding="utf-8") as f:
                return json.load(f)
        except Exception:
            pass
    return default


def save_json_file(path, data):
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def load_store():
    return load_json_file(DATA_FILE, {})


def save_store():
    save_json_file(DATA_FILE, domain_store)


def load_users():
    return load_json_file(USERS_FILE, [])


def save_users():
    save_json_file(USERS_FILE, users_store)


domain_store = load_store()
users_store = load_users()


# ─── Auth Helpers ───
def current_user():
    uid = session.get("user_id")
    if not uid:
        return None
    for user in users_store:
        if user["id"] == uid:
            return user
    return None


def login_required(view):
    @wraps(view)
    def wrapped(*args, **kwargs):
        user = current_user()
        if not user:
            if request.path.startswith("/api/"):
                return jsonify({"error": "Unauthorized"}), 401
            return redirect(url_for("login_page"))
        return view(*args, **kwargs)
    return wrapped


def guest_only(view):
    @wraps(view)
    def wrapped(*args, **kwargs):
        if current_user():
            return redirect(url_for("index"))
        return view(*args, **kwargs)
    return wrapped


# ─── Helpers ───
def parse_status(exp_date_str, today=None):
    if today is None:
        today = date.today()
    s = (exp_date_str or "").strip().lower()
    if not s or s in ("not been registered", "not registered"):
        return "not_registered"
    if s in ("invalid", "unknown", "invalid/unknown"):
        return "invalid"
    try:
        exp = datetime.strptime(exp_date_str.strip(), "%Y-%m-%d").date()
        delta = (exp - today).days
        if delta < 0:
            return "expired"
        elif delta <= 30:
            return "expiring_soon"
        return "active"
    except Exception:
        return "invalid"


def parse_txt_content(content, group_name):
    domains = []
    lines = content.replace("\r\n", "\n").replace("\r", "\n").split("\n")
    for line in lines:
        line = line.strip()
        if not line or line.startswith("_") or re.match(r"^(Exp |Not |Invalid)", line, re.I):
            continue
        m = re.match(r"Domain:\s*(.+?),\s*Exp Date:\s*(.+)", line, re.IGNORECASE)
        if m:
            domain = m.group(1).strip()
            exp_date = m.group(2).strip()
            if exp_date.lower() in ("not been registered", "not registered"):
                status, exp_date = "not_registered", ""
            elif exp_date.lower() in ("invalid", "unknown", "invalid/unknown"):
                status, exp_date = "invalid", ""
            else:
                status = parse_status(exp_date)
            domains.append({
                "group": group_name,
                "domain": domain,
                "exp_date": exp_date,
                "status": status,
                "source": "txt",
                "notes": ""
            })
    return domains


def rdap_lookup(domain):
    try:
        resp = requests.get(f"https://rdap.org/domain/{domain}", timeout=10)
        if resp.status_code == 404:
            return {"exp_date": "", "status": "not_registered"}
        if resp.status_code != 200:
            return {"exp_date": "", "status": "invalid"}
        data = resp.json()
        exp_date = ""
        for ev in data.get("events", []):
            if ev.get("eventAction") == "expiration":
                try:
                    dt = datetime.fromisoformat(ev["eventDate"].replace("Z", "+00:00"))
                    exp_date = dt.strftime("%Y-%m-%d")
                except Exception:
                    pass
        status = parse_status(exp_date) if exp_date else "active"
        return {"exp_date": exp_date, "status": status}
    except Exception:
        return {"exp_date": "", "status": "invalid"}


# ─── Page Routes ───
@app.route("/")
@login_required
def index():
    return render_template("index.html", auth_user=current_user())


@app.route("/login")
@guest_only
def login_page():
    return render_template("login.html")


@app.route("/register")
@guest_only
def register_page():
    return render_template("register.html")


@app.route("/logout")
@login_required
def logout():
    session.clear()
    return redirect(url_for("login_page"))


# ─── Auth API ───
@app.route("/api/auth/register", methods=["POST"])
def auth_register():
    global users_store
    data = request.get_json() or {}

    name = (data.get("name") or "").strip()
    username = (data.get("username") or "").strip().lower()
    password = data.get("password") or ""
    confirm = data.get("confirm_password") or ""

    if not name or not username or not password:
        return jsonify({"error": "Nama, username, dan password wajib diisi"}), 400

    if len(username) < 3 or not re.match(r"^[a-z0-9._-]+$", username):
        return jsonify({"error": "Username minimal 3 karakter dan hanya boleh huruf kecil, angka, titik, strip, underscore"}), 400

    if len(password) < 6:
        return jsonify({"error": "Password minimal 6 karakter"}), 400

    if password != confirm:
        return jsonify({"error": "Konfirmasi password tidak sama"}), 400

    if any(u["username"] == username for u in users_store):
        return jsonify({"error": "Username sudah terdaftar"}), 400

    user = {
        "id": secrets.token_hex(16),
        "name": name,
        "username": username,
        "password_hash": generate_password_hash(password),
        "created_at": datetime.utcnow().isoformat()
    }

    users_store.append(user)
    save_users()

    return jsonify({"ok": True, "message": "Register berhasil"})


@app.route("/api/auth/login", methods=["POST"])
def auth_login():
    data = request.get_json() or {}
    username = (data.get("username") or "").strip().lower()
    password = data.get("password") or ""

    if not username or not password:
        return jsonify({"error": "Username dan password wajib diisi"}), 400

    user = next((u for u in users_store if u["username"] == username), None)
    if not user or not check_password_hash(user["password_hash"], password):
        return jsonify({"error": "Username atau password salah"}), 401

    session.clear()
    session["user_id"] = user["id"]
    session["username"] = user["username"]
    session["name"] = user["name"]

    return jsonify({
        "ok": True,
        "user": {
            "id": user["id"],
            "name": user["name"],
            "username": user["username"]
        }
    })


@app.route("/api/auth/me")
@login_required
def auth_me():
    user = current_user()
    return jsonify({
        "id": user["id"],
        "name": user["name"],
        "username": user["username"]
    })


# ─── Protected Routes ───
@app.route("/api/groups")
@login_required
def get_groups():
    result = {}
    for g, items in domain_store.items():
        result[g] = {
            "count": len(items),
            "active": sum(1 for x in items if x["status"] == "active"),
            "expired": sum(1 for x in items if x["status"] == "expired"),
            "expiring_soon": sum(1 for x in items if x["status"] == "expiring_soon"),
            "not_registered": sum(1 for x in items if x["status"] == "not_registered"),
            "invalid": sum(1 for x in items if x["status"] == "invalid"),
        }
    return jsonify(result)


@app.route("/api/domains")
@login_required
def get_domains():
    group = request.args.get("group", "")
    status_filter = request.args.get("status", "")
    search = request.args.get("search", "").lower()
    result = []
    groups = [group] if group and group in domain_store else list(domain_store.keys())
    for g in groups:
        for item in domain_store.get(g, []):
            if status_filter and item["status"] != status_filter:
                continue
            if search and search not in item["domain"].lower():
                continue
            result.append(item)
    return jsonify(result)


@app.route("/api/upload", methods=["POST"])
@login_required
def upload_txt():
    group_name = request.form.get("group", "")
    mode = request.form.get("mode", "append")
    file = request.files.get("file")
    if not file:
        return jsonify({"error": "No file"}), 400
    content = file.read().decode("utf-8", errors="ignore")
    if not group_name:
        group_name = file.filename.rsplit(".", 1)[0]
    domains = parse_txt_content(content, group_name)
    if mode == "replace" or group_name not in domain_store:
        domain_store[group_name] = domains
    else:
        existing = {d["domain"] for d in domain_store[group_name]}
        for d in domains:
            if d["domain"] not in existing:
                domain_store[group_name].append(d)
    save_store()
    return jsonify({"added": len(domains), "group": group_name, "total": len(domain_store[group_name])})


@app.route("/api/add", methods=["POST"])
@login_required
def add_domain():
    data = request.json
    group = data.get("group", "Default")
    domain = data.get("domain", "").strip().lower()
    exp_date = data.get("exp_date", "").strip()
    notes = data.get("notes", "")
    if not domain:
        return jsonify({"error": "Domain required"}), 400
    status = parse_status(exp_date) if exp_date else "active"
    entry = {
        "domain": domain, "exp_date": exp_date, "status": status,
        "source": "manual", "notes": notes, "group": group
    }
    domain_store.setdefault(group, []).append(entry)
    save_store()
    return jsonify(entry)


@app.route("/api/edit", methods=["POST"])
@login_required
def edit_domain():
    data = request.json
    group = data.get("group")
    domain = data.get("domain")
    if group not in domain_store:
        return jsonify({"error": "Group not found"}), 404
    for item in domain_store[group]:
        if item["domain"] == domain:
            item["exp_date"] = data.get("exp_date", "").strip()
            item["notes"] = data.get("notes", "")
            override = data.get("status_override", "")
            item["status"] = override if override else parse_status(item["exp_date"])
            save_store()
            return jsonify(item)
    return jsonify({"error": "Domain not found"}), 404


@app.route("/api/delete", methods=["POST"])
@login_required
def delete_domain():
    data = request.json
    group = data.get("group")
    domain = data.get("domain")
    if group in domain_store:
        domain_store[group] = [x for x in domain_store[group] if x["domain"] != domain]
        save_store()
    return jsonify({"ok": True})


@app.route("/api/delete_group", methods=["POST"])
@login_required
def delete_group():
    data = request.json
    group = data.get("group")
    if group in domain_store:
        del domain_store[group]
        save_store()
    return jsonify({"ok": True})


@app.route("/api/rdap", methods=["POST"])
@login_required
def check_rdap():
    data = request.json
    domain = data.get("domain", "").strip()
    group = data.get("group", "Default")
    if not domain:
        return jsonify({"error": "Domain required"}), 400
    result = rdap_lookup(domain)
    entry = {
        "domain": domain, "exp_date": result["exp_date"],
        "status": result["status"], "source": "rdap",
        "notes": "", "group": group
    }
    domain_store.setdefault(group, [])
    found = False
    for item in domain_store[group]:
        if item["domain"] == domain:
            item.update({"exp_date": result["exp_date"], "status": result["status"], "source": "rdap"})
            found = True
            break
    if not found:
        domain_store[group].append(entry)
    save_store()
    return jsonify(entry)


@app.route("/api/recheck", methods=["POST"])
@login_required
def recheck_domain():
    data = request.json
    group = data.get("group")
    domain = data.get("domain")
    result = rdap_lookup(domain)
    if group in domain_store:
        for item in domain_store[group]:
            if item["domain"] == domain:
                item.update({"exp_date": result["exp_date"], "status": result["status"], "source": "rdap"})
                save_store()
                return jsonify(item)
    return jsonify({"error": "Not found"}), 404


@app.route("/data/domains.json")
@login_required
def serve_json():
    if os.path.exists(DATA_FILE):
        return send_file(DATA_FILE, mimetype="application/json")
    return jsonify({}), 200


@app.route("/api/export")
@login_required
def export_domains():
    fmt = request.args.get("format", "csv")
    group = request.args.get("group", "")
    status_filter = request.args.get("status", "")
    groups = [group] if group and group in domain_store else list(domain_store.keys())
    rows = []
    for g in groups:
        for item in domain_store.get(g, []):
            if status_filter and item["status"] != status_filter:
                continue
            rows.append(item)

    if fmt == "txt":
        lines = []
        current_group = None
        for r in rows:
            if r["group"] != current_group:
                current_group = r["group"]
                lines.append(f"\n=== {current_group} ===")
                lines.append("_" * 72)
            ed = r["exp_date"] if r["exp_date"] else r["status"].replace("_", " ").title()
            lines.append(f"Domain: {r['domain']}, Exp Date: {ed}")
        buf = io.BytesIO("\n".join(lines).encode("utf-8"))
        buf.seek(0)
        return send_file(buf, mimetype="text/plain", as_attachment=True, download_name="domains.txt")

    if fmt == "csv":
        buf = io.StringIO()
        writer = csv.writer(buf)
        writer.writerow(["Group", "Domain", "Exp Date", "Status", "Source", "Notes"])
        for r in rows:
            writer.writerow([r["group"], r["domain"], r["exp_date"], r["status"], r["source"], r["notes"]])
        buf.seek(0)
        return send_file(
            io.BytesIO(buf.getvalue().encode("utf-8")),
            mimetype="text/csv", as_attachment=True, download_name="domains.csv"
        )

    if fmt == "xlsx":
        wb = Workbook()
        ws = wb.active
        ws.title = "Domains"
        headers = ["Group", "Domain", "Exp Date", "Status", "Days Left", "Source", "Notes"]
        ws.append(headers)
        for cell in ws[1]:
            cell.font = Font(bold=True, color="FFFFFF")
            cell.fill = PatternFill("solid", fgColor="01696F")
            cell.alignment = Alignment(horizontal="center", vertical="center")
        status_colors = {
            "active": "D4EFCC", "expiring_soon": "FFF3CD",
            "expired": "F8D7DA", "not_registered": "E2E3E5", "invalid": "F5C6CB"
        }
        today = date.today()
        for r in rows:
            days_left = ""
            if r["exp_date"]:
                try:
                    exp = datetime.strptime(r["exp_date"], "%Y-%m-%d").date()
                    days_left = (exp - today).days
                except Exception:
                    pass
            ws.append([
                r["group"], r["domain"], r["exp_date"],
                r["status"].replace("_", " ").title(),
                days_left, r["source"], r["notes"]
            ])
            fill = PatternFill("solid", fgColor=status_colors.get(r["status"], "FFFFFF"))
            for col in range(1, 8):
                ws.cell(row=ws.max_row, column=col).fill = fill
        for i, w in enumerate([20, 35, 15, 18, 12, 12, 30], 1):
            ws.column_dimensions[get_column_letter(i)].width = w
        buf = io.BytesIO()
        wb.save(buf)
        buf.seek(0)
        return send_file(
            buf,
            mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            as_attachment=True, download_name="domains.xlsx"
        )

    return jsonify({"error": "Unknown format"}), 400


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5050)