import os
import re
import csv
import random
from io import StringIO
from uuid import uuid4
from datetime import datetime, timezone
from flask import Flask, render_template, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import text
from proxy_checker import check_proxies_concurrently
import sim5_client

app = Flask(__name__)

# Use absolute path for SQLite so it works correctly under Passenger/cPanel
base_dir = os.path.dirname(os.path.abspath(__file__))
db_path = os.path.join(base_dir, "proxies.db")
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = "change-me-in-production"

db = SQLAlchemy(app)


# =======================
# DATABASE MODELS
# =======================

class Batch(db.Model):
    __tablename__ = "batch"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=True)
    created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
    total = db.Column(db.Integer, default=0)
    processed_count = db.Column(db.Integer, default=0)
    clean_count = db.Column(db.Integer, default=0)
    status = db.Column(db.String(20), default="running")

    proxies = db.relationship("ProxyResult", backref="batch", lazy=True, cascade="all, delete-orphan")

    def to_dict(self):
        return {
            "id": self.id,
            "name": self.name,
            "created_at": self.created_at.isoformat() if self.created_at else None,
            "total": self.total,
            "processed_count": self.processed_count,
            "clean_count": self.clean_count,
            "status": self.status,
        }


class ProxyResult(db.Model):
    __tablename__ = "proxy_result"
    id = db.Column(db.Integer, primary_key=True)
    batch_id = db.Column(db.Integer, db.ForeignKey("batch.id"), nullable=False)
    proxy_string = db.Column(db.Text, nullable=False)
    ip = db.Column(db.String(50))
    country = db.Column(db.String(100))
    region = db.Column(db.String(100))
    city = db.Column(db.String(100))
    zip_code = db.Column(db.String(20))
    fraud_score = db.Column(db.Integer)
    is_clean = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))

    def to_dict(self):
        return {
            "id": self.id,
            "batch_id": self.batch_id,
            "proxy_string": self.proxy_string,
            "ip": self.ip,
            "country": self.country,
            "region": self.region,
            "city": self.city,
            "zip_code": self.zip_code,
            "fraud_score": self.fraud_score,
            "is_clean": self.is_clean,
            "created_at": self.created_at.isoformat() if self.created_at else None,
        }


class EmailPool(db.Model):
    __tablename__ = "email_pool"
    id = db.Column(db.Integer, primary_key=True)
    first_name = db.Column(db.String(100))
    last_name = db.Column(db.String(100))
    email = db.Column(db.String(200), nullable=False)
    used = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))

    def to_dict(self):
        return {
            "id": self.id,
            "first_name": self.first_name,
            "last_name": self.last_name,
            "email": self.email,
            "used": self.used,
            "created_at": self.created_at.isoformat() if self.created_at else None,
        }


class SimOrder(db.Model):
    __tablename__ = "sim_order"
    id = db.Column(db.Integer, primary_key=True)
    order_id = db.Column(db.BigInteger, nullable=False, unique=True)
    phone = db.Column(db.String(50))
    product = db.Column(db.String(100))
    country = db.Column(db.String(100))
    operator = db.Column(db.String(100))
    price = db.Column(db.Float)
    status = db.Column(db.String(50))
    sms_data = db.Column(db.Text)  # JSON string
    created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
    updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))

    def to_dict(self):
        return {
            "id": self.id,
            "order_id": self.order_id,
            "phone": self.phone,
            "product": self.product,
            "country": self.country,
            "operator": self.operator,
            "price": self.price,
            "status": self.status,
            "sms_data": self.sms_data,
            "created_at": self.created_at.isoformat() if self.created_at else None,
            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
        }


class NodeMavenConfig(db.Model):
    __tablename__ = "nodemaven_config"
    id = db.Column(db.Integer, primary_key=True)
    host = db.Column(db.String(100), default="gate.nodemaven.com")
    port = db.Column(db.Integer, default=8080)
    username = db.Column(db.String(200))
    password = db.Column(db.String(200))
    default_country = db.Column(db.String(10), default="us")
    default_region = db.Column(db.String(100), default="")
    default_count = db.Column(db.Integer, default=10)
    updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))

    def to_dict(self):
        return {
            "id": self.id,
            "host": self.host,
            "port": self.port,
            "username": self.username,
            "password": self.password,
            "default_country": self.default_country,
            "default_region": self.default_region,
            "default_count": self.default_count,
        }


# =======================
# HELPERS
# =======================

def parse_proxies(text: str):
    lines = []
    for line in text.strip().splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        lines.append(line)
    return lines


def parse_email_csv(text: str):
    emails = []
    f = StringIO(text.strip())
    reader = csv.reader(f)
    header = None
    for row in reader:
        if not row:
            continue
        if header is None:
            header = [h.strip().lower() for h in row]
            continue
        if len(row) < 3:
            continue
        first = last = email = ""
        if "first" in header and "last" in header and "email" in header:
            try:
                first = row[header.index("first")].strip()
                last = row[header.index("last")].strip()
                email = row[header.index("email")].strip()
            except IndexError:
                continue
        else:
            first = row[0].strip()
            last = row[1].strip()
            email = row[2].strip()
        if email and "@" in email:
            emails.append({"first_name": first, "last_name": last, "email": email})
    return emails


# =======================
# ROUTES
# =======================

@app.route("/")
def index():
    return render_template("index.html")


# ---------- PROXY ROUTES ----------

@app.route("/api/batches", methods=["GET", "POST"])
def batches():
    if request.method == "POST":
        data = request.get_json() or {}
        name = data.get("name", f"Batch {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')}")
        batch = Batch(name=name, status="running")
        db.session.add(batch)
        db.session.commit()
        return jsonify(batch.to_dict()), 201

    all_batches = Batch.query.order_by(Batch.created_at.desc()).all()
    return jsonify([b.to_dict() for b in all_batches])


@app.route("/api/batches/<int:batch_id>")
def get_batch(batch_id):
    batch = db.session.get(Batch, batch_id)
    if not batch:
        return jsonify({"error": "Not found"}), 404
    return jsonify(batch.to_dict())


@app.route("/api/batches/<int:batch_id>/proxies")
def get_batch_proxies(batch_id):
    batch = db.session.get(Batch, batch_id)
    if not batch:
        return jsonify({"error": "Not found"}), 404
    is_clean = request.args.get("is_clean")
    query = ProxyResult.query.filter_by(batch_id=batch_id)
    if is_clean is not None:
        query = query.filter_by(is_clean=(is_clean.lower() == "true"))
    proxies = query.order_by(ProxyResult.id.asc()).all()
    return jsonify([p.to_dict() for p in proxies])


@app.route("/api/proxies")
def get_all_proxies():
    query = ProxyResult.query
    is_clean = request.args.get("is_clean")
    search = request.args.get("search")

    if is_clean is not None:
        query = query.filter_by(is_clean=(is_clean.lower() == "true"))
    if search:
        query = query.filter(ProxyResult.proxy_string.ilike(f"%{search}%"))

    proxies = query.order_by(ProxyResult.created_at.desc()).limit(1000).all()
    return jsonify([p.to_dict() for p in proxies])


@app.route("/api/check", methods=["POST"])
def start_check():
    data = request.get_json() or {}
    raw_text = data.get("proxies", "")
    name = data.get("name", f"Batch {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')}")

    proxy_lines = parse_proxies(raw_text)
    if not proxy_lines:
        return jsonify({"error": "No valid proxies provided"}), 400

    batch = Batch(name=name, total=len(proxy_lines), status="running")
    db.session.add(batch)
    db.session.commit()

    def progress_callback(completed, total, result):
        b = db.session.get(Batch, batch.id)
        b.processed_count += 1

        # Only save clean proxies (fraud_score == 0)
        if result.get("status") != "failed" and result.get("is_clean"):
            pr = ProxyResult(
                batch_id=batch.id,
                proxy_string=result["proxy"],
                ip=result.get("ip"),
                country=result.get("country"),
                region=result.get("region"),
                city=result.get("city"),
                zip_code=result.get("zip_code"),
                fraud_score=result.get("fraud_score"),
                is_clean=True,
            )
            db.session.add(pr)
            b.clean_count += 1

        db.session.commit()

    import threading
    def run_checks():
        with app.app_context():
            try:
                check_proxies_concurrently(proxy_lines, max_workers=10, progress_callback=progress_callback)
                b = db.session.get(Batch, batch.id)
                b.status = "completed"
            except Exception as e:
                b = db.session.get(Batch, batch.id)
                b.status = "error"
                app.logger.error(f"Check failed: {e}")
            db.session.commit()

    thread = threading.Thread(target=run_checks, daemon=True)
    thread.start()

    return jsonify({"batch_id": batch.id, "total": len(proxy_lines), "status": "started"})


@app.route("/api/batches/<int:batch_id>/progress")
def batch_progress(batch_id):
    batch = db.session.get(Batch, batch_id)
    if not batch:
        return jsonify({"error": "Not found"}), 404
    return jsonify({
        "batch_id": batch.id,
        "status": batch.status,
        "total": batch.total,
        "checked": batch.processed_count,
        "clean_count": batch.clean_count,
        "percent": round((batch.processed_count / batch.total) * 100, 1) if batch.total else 0,
    })


@app.route("/api/batches/<int:batch_id>/clean")
def get_clean_proxies(batch_id):
    batch = db.session.get(Batch, batch_id)
    if not batch:
        return jsonify({"error": "Not found"}), 404
    clean = ProxyResult.query.filter_by(batch_id=batch_id, is_clean=True).all()
    lines = [p.proxy_string for p in clean]
    return jsonify({"proxies": lines})


@app.route("/api/batches/<int:batch_id>", methods=["DELETE"])
def delete_batch(batch_id):
    batch = db.session.get(Batch, batch_id)
    if not batch:
        return jsonify({"error": "Not found"}), 404
    db.session.delete(batch)
    db.session.commit()
    return jsonify({"deleted": True})


# ---------- EMAIL ROUTES ----------

@app.route("/api/emails/upload", methods=["POST"])
def upload_emails():
    data = request.get_json() or {}
    raw_text = data.get("csv", "")
    if not raw_text.strip():
        return jsonify({"error": "No CSV data provided"}), 400

    emails = parse_email_csv(raw_text)
    if not emails:
        return jsonify({"error": "No valid emails found in CSV"}), 400

    added = 0
    for e in emails:
        existing = EmailPool.query.filter_by(email=e["email"]).first()
        if not existing:
            ep = EmailPool(first_name=e["first_name"], last_name=e["last_name"], email=e["email"])
            db.session.add(ep)
            added += 1

    db.session.commit()
    total_remaining = EmailPool.query.filter_by(used=False).count()
    return jsonify({"added": added, "total_remaining": total_remaining})


@app.route("/api/emails/select", methods=["POST"])
def select_emails():
    data = request.get_json() or {}
    count = data.get("count", 25)
    if not (20 <= count <= 30):
        count = 25

    available = EmailPool.query.filter_by(used=False).all()
    if len(available) < count:
        count = len(available)

    if count == 0:
        return jsonify({"error": "No remaining emails in pool"}), 400

    selected = random.sample(available, count)
    for ep in selected:
        ep.used = True

    db.session.commit()
    total_remaining = EmailPool.query.filter_by(used=False).count()

    return jsonify({
        "selected": [e.to_dict() for e in selected],
        "total_remaining": total_remaining,
    })


@app.route("/api/emails/remaining")
def remaining_emails():
    total = EmailPool.query.filter_by(used=False).count()
    return jsonify({"total_remaining": total})


@app.route("/api/emails", methods=["DELETE"])
def clear_emails():
    EmailPool.query.delete()
    db.session.commit()
    return jsonify({"cleared": True})


# ---------- 5SIM ROUTES ----------

@app.route("/api/5sim/profile")
def sim5_profile():
    try:
        data = sim5_client.get_profile()
        return jsonify(data)
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/5sim/countries")
def sim5_countries():
    try:
        data = sim5_client.get_countries()
        return jsonify(data)
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/5sim/products")
def sim5_products():
    country = request.args.get("country", "")
    product = request.args.get("product", "")
    try:
        data = sim5_client.get_products(country=country, product=product)
        return jsonify(data)
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/5sim/buy", methods=["POST"])
def sim5_buy():
    data = request.get_json() or {}
    country = data.get("country", "")
    operator = data.get("operator", "any")
    product = data.get("product", "")

    if not country or not product:
        return jsonify({"error": "Country and product are required"}), 400

    try:
        result = sim5_client.buy_activation_number(country, operator, product)
        # Store locally
        so = SimOrder(
            order_id=result["id"],
            phone=result.get("phone"),
            product=result.get("product"),
            country=result.get("country"),
            operator=result.get("operator"),
            price=result.get("price"),
            status=result.get("status"),
        )
        db.session.add(so)
        db.session.commit()
        return jsonify(result)
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/5sim/check/<int:order_id>")
def sim5_check(order_id):
    try:
        data = sim5_client.check_order(order_id)
        # Update local record
        so = SimOrder.query.filter_by(order_id=order_id).first()
        if so:
            so.status = data.get("status", so.status)
            import json
            so.sms_data = json.dumps(data.get("sms", []))
            db.session.commit()
        return jsonify(data)
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/5sim/finish/<int:order_id>", methods=["POST"])
def sim5_finish(order_id):
    try:
        data = sim5_client.finish_order(order_id)
        so = SimOrder.query.filter_by(order_id=order_id).first()
        if so:
            so.status = data.get("status", so.status)
            db.session.commit()
        return jsonify(data)
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/5sim/cancel/<int:order_id>", methods=["POST"])
def sim5_cancel(order_id):
    try:
        data = sim5_client.cancel_order(order_id)
        so = SimOrder.query.filter_by(order_id=order_id).first()
        if so:
            so.status = data.get("status", so.status)
            db.session.commit()
        return jsonify(data)
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/5sim/orders")
def sim5_orders():
    category = request.args.get("category", "activation")
    try:
        data = sim5_client.get_orders(category=category)
        return jsonify(data)
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/5sim/local-orders")
def sim5_local_orders():
    orders = SimOrder.query.order_by(SimOrder.created_at.desc()).all()
    return jsonify([o.to_dict() for o in orders])


# ---------- NODEMAVEN ROUTES ----------

@app.route("/api/nodemaven/config", methods=["GET", "POST"])
def nodemaven_config():
    cfg = NodeMavenConfig.query.first()
    if request.method == "POST":
        data = request.get_json() or {}
        if not cfg:
            cfg = NodeMavenConfig()
            db.session.add(cfg)
        cfg.host = data.get("host", cfg.host if cfg else "gate.nodemaven.com")
        cfg.port = data.get("port", cfg.port if cfg else 8080)
        cfg.username = data.get("username", cfg.username if cfg else "")
        cfg.password = data.get("password", cfg.password if cfg else "")
        cfg.default_country = data.get("default_country", cfg.default_country if cfg else "us")
        cfg.default_region = data.get("default_region", cfg.default_region if cfg else "")
        cfg.default_count = data.get("default_count", cfg.default_count if cfg else 10)
        db.session.commit()
        return jsonify(cfg.to_dict())
    # GET
    if cfg:
        return jsonify(cfg.to_dict())
    return jsonify({"host": "gate.nodemaven.com", "port": 8080, "username": "", "password": "", "default_country": "us", "default_region": "california", "default_count": 10})


@app.route("/api/nodemaven/generate", methods=["POST"])
def nodemaven_generate():
    data = request.get_json() or {}
    cfg = NodeMavenConfig.query.first()
    if not cfg or not cfg.username or not cfg.password:
        return jsonify({"error": "NodeMaven credentials not configured"}), 400

    country = data.get("country", "us")
    region = data.get("region", "")
    count = int(data.get("count", 10))
    filter_level = "medium"

    proxies = []
    for i in range(count):
        sid = uuid4().hex[:16]
        params = [f"country-{country}", f"sid-{sid}", f"filter-{filter_level}"]
        if region:
            params.insert(1, f"region-{region}")
        username = f"{cfg.username}-{'-'.join(params)}"
        proxy = f"{cfg.host}:{cfg.port}:{username}:{cfg.password}"
        proxies.append(proxy)

    return jsonify({"proxies": proxies})


# =======================
# INIT
# =======================

def _migrate_db():
    """Add missing columns to nodemaven_config if upgrading from older schema."""
    cols = [
        ("default_country", "VARCHAR(10) DEFAULT 'us'"),
        ("default_region", "VARCHAR(100) DEFAULT ''"),
        ("default_count", "INTEGER DEFAULT 10"),
    ]
    for col_name, col_def in cols:
        try:
            db.session.execute(text(f"ALTER TABLE nodemaven_config ADD COLUMN {col_name} {col_def}"))
            db.session.commit()
        except Exception:
            db.session.rollback()

with app.app_context():
    db.create_all()
    _migrate_db()

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)
