from fastapi import FastAPI, UploadFile, File, Form
from pathlib import Path
import shutil
import json
import hashlib
from fastapi.responses import FileResponse
import zipfile
from jose import JWTError, jwt
from datetime import datetime, timedelta
from passlib.context import CryptContext
from fastapi import Header, HTTPException
from fastapi import Depends, Header, HTTPException
from nebula.neb_server.auth import (
    create_access_token,
    verify_token,
    verify_password,
    hash_password
)
import smtplib
from email.message import EmailMessage
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy import UniqueConstraint
from fastapi import Body
from sqlalchemy import DateTime
from datetime import datetime

app = FastAPI()

SECRET_KEY = "asdfajgfnoieghklmdvs0934"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60
SERVER_ROOT = Path("projects")
SERVER_ROOT.mkdir(exist_ok=True)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def send_invite_email(email, project_code, invite_code):
    html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {{
    margin: 0;
    padding: 0;
    background-color: #f4f6fb;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}}

.container {{
    max-width: 600px;
    margin: 40px auto;
    background: #ffffff;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 8px 24px rgba(0,0,0,0.08);
}}

.header {{
    background: linear-gradient(135deg, #6366f1, #7c3aed);
    color: white;
    padding: 28px;
    text-align: center;
    font-size: 22px;
    font-weight: 600;
}}

.content {{
    padding: 32px;
    color: #333;
    line-height: 1.6;
}}

.project-box {{
    background: #f1f5f9;
    padding: 16px;
    border-radius: 8px;
    margin: 20px 0;
    font-family: monospace;
    font-size: 14px;
}}

.button {{
    display: inline-block;
    margin-top: 20px;
    padding: 12px 22px;
    background: #6366f1;
    color: #fff;
    text-decoration: none;
    border-radius: 8px;
    font-weight: 500;
}}

.steps {{
    background: #f8fafc;
    padding: 16px;
    border-radius: 8px;
    margin-top: 20px;
}}

.footer {{
    text-align: center;
    font-size: 12px;
    color: #888;
    padding: 20px;
}}
</style>
</head>

<body>

<div class="container">

<div class="header">
🚀 Nebula Project Invitation
</div>

<div class="content">

<p>Hello,</p>

<p>You’ve been invited to collaborate on a Nebula project.</p>

<div class="project-box">
<b>Project Code:</b> {project_code}<br>
<b>Invite Code:</b> {invite_code}
</div>

<p>Use the details above to join the project securely.</p>

<div class="steps">
<b>Getting Started:</b><br><br>
1. Install Nebula CLI<br>
2. Run <b>nb signup</b><br>
3. Run <b>nb login</b><br>
4. Run <b>nb joinproject</b><br>
</div>

<a href="https://pypi.org/project/nebula-nb/" class="button">
Install Nebula CLI
</a>

<p style="margin-top: 24px;">
If you didn’t expect this invite, you can safely ignore this email.
</p>

</div>

<div class="footer">
Nebula • Modern Version Control System
</div>

</div>

</body>
</html>
"""

    msg = EmailMessage()
    msg["Subject"] = "🚀 You're invited to a Nebula project"
    msg["From"] = "Nebula <uditkhare214@gmail.com>"
    msg["To"] = email

    # Plain fallback
    msg.set_content(f"""
You are invited to a Nebula project.

Project Code: {project_code}
Invite Code: {invite_code}

Run:
nb signup
nb login
nb joinproject
""")

    # HTML version
    msg.add_alternative(html_content, subtype="html")

    with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
        smtp.starttls()
        smtp.login("uditkhare214@gmail.com", "spds nxuh mkdh bdlj")
        smtp.send_message(msg)

# ---------------- DATABASE SETUP ----------------


DATABASE_URL = "sqlite:///./nebula.db"

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False}
)

SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()

# ---------------- USER MODEL ----------------
class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True)
    username = Column(String, unique=True)
    password = Column(String)

class Project(Base):
    __tablename__ = "projects"

    id = Column(Integer, primary_key=True)
    project_code = Column(String, unique=True, index=True)
    project_name = Column(String, index=True)   # ✅ NEW
    owner = Column(String, index=True)

    # ✅ composite uniqueness
    __table_args__ = (
        UniqueConstraint("owner", "project_name", name="uix_owner_project"),
    )
class Invite(Base):
    __tablename__ = "invites"

    id = Column(Integer, primary_key=True)
    project_code = Column(String, index=True)
    email = Column(String, index=True)
    invite_code = Column(String, unique=True)

    status = Column(String, default="pending")  
    # pending | accepted | rejected

    created_at = Column(DateTime, default=datetime.utcnow)

class Collaborator(Base):
    __tablename__ = "collaborators"

    id = Column(Integer, primary_key=True)
    project_code = Column(String, index=True)
    username = Column(String, index=True)
    role = Column(String, default="developer")  # future use

# CREATE TABLES
Base.metadata.create_all(bind=engine)

def get_current_user(authorization: str = Header(None)):

    if not authorization:
        raise HTTPException(status_code=401, detail="Missing token")

    token = authorization.replace("Bearer ", "")
    payload = verify_token(token)

    if not payload:
        raise HTTPException(status_code=401, detail="Invalid token")

    return payload



@app.get("/")
def root():
    return {"status": "Nebula server running"}


def get_hash(path: Path):
    hasher = hashlib.md5()

    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hasher.update(chunk)

    return hasher.hexdigest()


@app.post("/project/upload")
async def upload_file(
    project_code: str = Form(...),
    relative_path: str = Form(...),
    file: UploadFile = File(...),
    user=Depends(get_current_user)
):

    project = SERVER_ROOT / project_code

    if not project.exists():
        return {"error": "project_not_found"}

    main = project / "main"
    hashes_file = project / "hashes.json"

    dest = main / relative_path
    dest.parent.mkdir(parents=True, exist_ok=True)

    with open(dest, "wb") as buffer:
        shutil.copyfileobj(file.file, buffer)

    # ------------------------
    # Update server hashes
    # ------------------------

    new_hash = get_hash(dest)

    if hashes_file.exists():

        with open(hashes_file) as f:
            hashes = json.load(f)

    else:
        hashes = {}

    hashes[str(relative_path)] = new_hash

    with open(hashes_file, "w") as f:
        json.dump(hashes, f, indent=4)

    return {
        "status": "uploaded",
        "path": relative_path,
        "hash": new_hash
    }

@app.get("/project/hashes/{project_code}")
def get_hashes(project_code: str):

    project = SERVER_ROOT / project_code
    hashes_file = project / "hashes.json"

    if not hashes_file.exists():
        return {}

    with open(hashes_file) as f:
        hashes = json.load(f)

    return hashes

@app.get("/project/hashes/{project_code}")
def get_project_hashes(project_code: str):

    project = SERVER_ROOT / project_code
    hashes_file = project / "hashes.json"

    if not project.exists():
        return {"error": "project_not_found"}

    if not hashes_file.exists():
        return {}

    with open(hashes_file) as f:
        hashes = json.load(f)

    return hashes

import smtplib
from email.message import EmailMessage

def send_welcome_email(email, username):

    msg = EmailMessage()
    msg["Subject"] = "Welcome to Nebula 🚀"
    msg["From"] = "Nebula <yourgmail@gmail.com>"
    msg["To"] = email

    msg.set_content(f"Welcome {username} to Nebula!")

    html = f"""
    <html>
    <body style="background:#0b0f1a;color:white;font-family:Arial;padding:20px;">
        <div style="max-width:600px;margin:auto;background:#111827;padding:30px;border-radius:10px;">
            <h1 style="color:#a78bfa;">🚀 Welcome to Nebula</h1>
            <p>Hello <b>{username}</b>,</p>
            <p>Your account has been created successfully.</p>
            <p>You can now create and collaborate on projects.</p>

            <a href="https://pypi.org/project/nebula-nb/"
               style="display:inline-block;padding:12px 20px;background:#7c3aed;color:white;text-decoration:none;border-radius:8px;">
               Get Started
            </a>

            <p style="margin-top:30px;font-size:12px;color:#aaa;">
                Nebula • Modern Version Control
            </p>
        </div>
    </body>
    </html>
    """

    msg.add_alternative(html, subtype="html")

    with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
        smtp.starttls()
        smtp.login("uditkhare214@gmail.com", "spds nxuh mkdh bdlj")
        smtp.send_message(msg)

@app.post("/project/create")
def create_project(
    data: dict = Body(...),
    user=Depends(get_current_user)
):

    db = SessionLocal()

    project_code = data.get("project_code")
    project_name = data.get("project_name")
    username = user.get("username")

    # -------------------------
    # VALIDATION
    # -------------------------
    if not project_code or not project_name:
        return {"error": "missing_fields"}

    # check duplicate name per user
    existing = db.query(Project).filter(
        Project.owner == username,
        Project.project_name == project_name
    ).first()

    if existing:
        return {"error": "project_name_exists"}

    project = SERVER_ROOT / project_code

    if project.exists():
        return {"status": "exists"}

    # -------------------------
    # CREATE FILE STRUCTURE
    # -------------------------
    (project / "main").mkdir(parents=True)
    (project / "updates").mkdir()

    with open(project / "hashes.json", "w") as f:
        json.dump({}, f, indent=4)

    # -------------------------
    # SAVE TO DB
    # -------------------------
    new_project = Project(
        project_code=project_code,
        project_name=project_name,
        owner=username
    )
    db.add(new_project)

    owner_collab = Collaborator(
        project_code=project_code,
        username=username,
        role="owner"
    )
    db.add(owner_collab)

    db.commit()

    return {
        "status": "created",
        "project_code": project_code,
        "project_name": project_name
    }


@app.get("/project/download/{project_code}")
def download_project(project_code: str):

    project = SERVER_ROOT / project_code
    main = project / "main"

    if not main.exists():
        return {"error": "project_not_found"}

    zip_path = project / "project.zip"

    with zipfile.ZipFile(zip_path, "w") as zipf:
        for file in main.rglob("*"):
            if file.is_file():
                zipf.write(file, file.relative_to(main))

    return FileResponse(zip_path, filename="project.zip")


@app.get("/updates/{project_code}")
def list_updates(project_code: str):

    project = SERVER_ROOT / project_code
    updates_dir = project / "updates"

    if not updates_dir.exists():
        return []

    updates = []

    for file in updates_dir.glob("*.json"):
        with open(file) as f:
            updates.append(json.load(f))

    return updates

@app.get("/project/file/{project_code}")
def get_file(project_code: str, path: str):

    project = SERVER_ROOT / project_code
    file_path = project / "main" / path

    if not file_path.exists():
        return {"error": "file_not_found"}

    return FileResponse(file_path)

@app.post("/project/upload")
async def upload_file(
    project_code: str = Form(...),
    relative_path: str = Form(...),
    file: UploadFile = File(...),
    user=Depends(get_current_user)
):

    project = SERVER_ROOT / project_code
    main = project / "main"

    dest = main / relative_path
    dest.parent.mkdir(parents=True, exist_ok=True)

    with open(dest, "wb") as buffer:
        shutil.copyfileobj(file.file, buffer)

    # update hashes
    hashes_file = project / "hashes.json"

    if hashes_file.exists():
        with open(hashes_file) as f:
            hashes = json.load(f)
    else:
        hashes = {}

    hashes[str(relative_path)] = get_hash(dest)

    with open(hashes_file, "w") as f:
        json.dump(hashes, f, indent=4)

    return {"status": "uploaded"}

@app.post("/update/publish")
def publish_update(data: dict, user=Depends(get_current_user)):

    db = SessionLocal()

    project_code = data["project_code"]
    username = user.get("username")

    # -------------------------
    # CHECK ACCESS
    # -------------------------
    collab = db.query(Collaborator).filter(
        Collaborator.project_code == project_code,
        Collaborator.username == username
    ).first()

    if not collab:
        raise HTTPException(status_code=403, detail="Not a collaborator")

    # -------------------------
    # ALLOW ONLY OWNER / ADMIN
    # -------------------------
    if collab.role not in ["owner", "admin"]:
        raise HTTPException(status_code=403, detail="No permission")

    # -------------------------
    # STORE UPDATE
    # -------------------------
    update = data["update"]

    project = SERVER_ROOT / project_code
    updates_dir = project / "updates"
    updates_dir.mkdir(exist_ok=True)

    update_id = update.get("id") or update.get("name")

    update_file = updates_dir / f"{update_id}.json"

    with open(update_file, "w") as f:
        json.dump(update, f, indent=4)

    return {"status": "stored"}

@app.post("/auth/login")
def login(data: dict):

    db = SessionLocal()

    email = data.get("email")
    password = data.get("password")

    if not email or not password:
        return {"error": "missing_email_or_password"}

    user = db.query(User).filter(User.email == email).first()

    if not user:
        return {"error": "user_not_found"}

    if not verify_password(password, user.password):
        return {"error": "wrong_password"}

    token = create_access_token({
        "username": user.username,
        "email": user.email
    })

    return {"access_token": token}
    
@app.post("/auth/signup")
def signup(data: dict):
    db = SessionLocal()

    # -------------------------
    # CHECK USER EXISTS
    # -------------------------
    existing = db.query(User).filter(User.username == data["username"]).first()
    if existing:
        return {"error": "user_exists"}

    # -------------------------
    # CREATE USER (ONLY HASH ONCE)
    # -------------------------
    user = User(
        email=data["email"],
        username=data["username"],
        password=hash_password(data["password"])   # ✅ ONLY HERE
    )

    db.add(user)
    db.commit()

    # -------------------------
    # SEND EMAIL
    # -------------------------
    send_welcome_email(data["email"], data["username"])

    return {"status": "created"}

@app.get("/projects")
def get_projects(user=Depends(get_current_user)):

    db = SessionLocal()
    username = user.get("username")

    owned = db.query(Project).filter(Project.owner == username).all()

    owned_list = [
        {
            "name": p.project_name,
            "code": p.project_code
        }
        for p in owned
    ]

    joined = db.query(Collaborator).filter(
        Collaborator.username == username,
        Collaborator.role != "owner"
    ).all()

    joined_list = []

    for c in joined:
        project = db.query(Project).filter(
            Project.project_code == c.project_code
        ).first()

        if project:
            joined_list.append({
                "name": project.project_name,
                "code": project.project_code
            })

    return {
        "owned": owned_list,
        "joined": joined_list
    }

@app.delete("/project/delete")
def delete_project(data: dict, user=Depends(get_current_user)):

    db = SessionLocal()

    project_code = data.get("project_code")
    password = data.get("password")

    username = user.get("username")

    # -------------------------
    # VERIFY USER PASSWORD
    # -------------------------
    user_obj = db.query(User).filter(User.username == username).first()

    if not user_obj or not verify_password(password, user_obj.password):
        raise HTTPException(status_code=401, detail="Invalid password")

    # -------------------------
    # CHECK PROJECT OWNERSHIP
    # -------------------------
    project = db.query(Project).filter(Project.project_code == project_code).first()

    if not project:
        return {"error": "project_not_found"}

    if project.owner != username:
        return {"error": "not_owner"}

    # -------------------------
    # DELETE FROM FILESYSTEM
    # -------------------------
    project_path = SERVER_ROOT / project_code

    if project_path.exists():
        shutil.rmtree(project_path)

    # -------------------------
    # DELETE FROM DATABASE
    # -------------------------
    db.delete(project)
    db.commit()

    return {"status": "deleted"}

@app.delete("/auth/delete")
def delete_account(data: dict, user=Depends(get_current_user)):

    db = SessionLocal()

    password = data.get("password")
    username = user.get("username")

    user_obj = db.query(User).filter(User.username == username).first()

    if not user_obj:
        return {"error": "user_not_found"}

    # -------------------------
    # VERIFY PASSWORD
    # -------------------------
    if not verify_password(password, user_obj.password):
        raise HTTPException(status_code=401, detail="Invalid password")

    # -------------------------
    # DELETE ALL USER PROJECTS
    # -------------------------
    projects = db.query(Project).filter(Project.owner == username).all()

    for p in projects:
        project_path = SERVER_ROOT / p.project_code

        if project_path.exists():
            shutil.rmtree(project_path)

        db.delete(p)

    # -------------------------
    # DELETE USER
    # -------------------------
    db.delete(user_obj)
    db.commit()

    return {"status": "account_deleted"}
@app.post("/project/add-collaborator")
def add_collaborator(data: dict, user=Depends(get_current_user)):

    db = SessionLocal()

    project_code = data.get("project_code")
    email = data.get("email")
    current_user = user.get("username")

    if not project_code or not email:
        return {"error": "missing_fields"}

    project = db.query(Project).filter(
        Project.project_code == project_code
    ).first()

    if not project:
        return {"error": "project_not_found"}

    if project.owner != current_user:
        return {"error": "not_allowed"}

    existing_invite = db.query(Invite).filter(
        Invite.project_code == project_code,
        Invite.email == email,
        Invite.status == "pending"
    ).first()

    if existing_invite:
        return {"status": "already_invited"}

    import secrets
    invite_code = secrets.token_hex(4)

    invite = Invite(
        project_code=project_code,
        email=email,
        invite_code=invite_code,
        status="pending"
    )

    db.add(invite)
    db.commit()

    try:
        send_invite_email(email, project_code, invite_code)
        print(f"📧 Invite email sent to {email}")

    except Exception as e:
        print("❌ EMAIL FAILED:", str(e))

    return {
        "status": "invited",
        "invite_code": invite_code   # useful for debugging
    }

@app.post("/project/invite")
def invite_user(data: dict, user=Depends(get_current_user)):

    db = SessionLocal()

    project_code = data.get("project_code")
    email = data.get("email")
    current_user = user.get("username")

    project = db.query(Project).filter(
        Project.project_code == project_code
    ).first()

    if not project:
        return {"error": "project_not_found"}

    if project.owner != current_user:
        return {"error": "not_allowed"}

    import secrets
    invite_code = secrets.token_hex(4)

    invite = Invite(
        project_code=project_code,
        email=email,
        invite_code=invite_code,
        status="pending"
    )

    db.add(invite)
    db.commit()

    send_invite_email(email, project_code, invite_code)

    return {"status": "invited"}

@app.post("/project/join")
def join_project(data: dict, user=Depends(get_current_user)):

    db = SessionLocal()

    project_code = data.get("project_code")
    invite_code = data.get("invite_code")
    email = user.get("email")
    username = user.get("username")

    invite = db.query(Invite).filter(
        Invite.project_code == project_code,
        Invite.invite_code == invite_code,
        Invite.status == "pending"
    ).first()

    if not invite:
        return {"error": "invalid_invite"}

    if invite.email != email:
        return {"error": "email_mismatch"}

    # add collaborator
    db.add(Collaborator(
        project_code=project_code,
        username=username,
        role="developer"
    ))

    invite.status = "accepted"

    db.commit()

    return {"status": "joined"}

@app.post("/project/reject")
def reject_invite(data: dict, user=Depends(get_current_user)):

    db = SessionLocal()

    invite_code = data.get("invite_code")
    email = user.get("email")

    invite = db.query(Invite).filter(
        Invite.invite_code == invite_code,
        Invite.email == email,
        Invite.status == "pending"
    ).first()

    if not invite:
        return {"error": "invalid_invite"}

    invite.status = "rejected"
    db.commit()

    return {"status": "rejected"}

@app.get("/project/collaborators/{project_code}")
def get_collaborators(project_code: str, user=Depends(get_current_user)):

    db = SessionLocal()

    # accepted
    collabs = db.query(Collaborator).filter(
        Collaborator.project_code == project_code
    ).all()

    accepted = [c.username for c in collabs]

    # invites
    invites = db.query(Invite).filter(
        Invite.project_code == project_code
    ).all()

    pending = [i.email for i in invites if i.status == "pending"]
    rejected = [i.email for i in invites if i.status == "rejected"]

    return {
        "accepted": accepted,
        "pending": pending,
        "rejected": rejected
    }