import os
import time
import json
import logging
from datetime import datetime, timezone, timedelta
from typing import List, Optional

from dotenv import load_dotenv
from pathlib import Path

ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / ".env")

from fastapi import FastAPI, APIRouter, Depends, HTTPException, Request
from starlette.middleware.cors import CORSMiddleware
from pydantic import BaseModel, EmailStr, Field
from sqlalchemy.orm import Session

from database import Base, engine, get_db
from models import (
    User, Migration, MigrationFolder, MigrationLog,
    Package, Subscription, SiteSetting, LoginLockout, Coupon,
    CloudAccount, CloudMigration, AppSetting, EmailTemplate, PasswordResetToken,
)
from security import (
    hash_password, verify_password, encrypt_secret,
    generate_totp_secret, totp_provisioning_uri, verify_totp, qr_data_url,
)
from auth import create_access_token, get_current_user
import imap_engine
import billing
import notifications
import cloud
import settings_store
import report
import jwt as _jwt

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger("mailshift")

app = FastAPI(title="MailShift API")
api = APIRouter(prefix="/api")

PROVIDERS = [
    {"id": "gmail", "name": "Gmail", "host": "imap.gmail.com", "port": 993, "ssl": True,
     "oauth": True, "note": "Sign in with Google — no password needed."},
    {"id": "office365", "name": "Office 365 / Outlook", "host": "outlook.office365.com", "port": 993, "ssl": True,
     "oauth": True, "note": "Sign in with Microsoft — Basic auth is disabled by Microsoft, OAuth is required."},
    {"id": "yahoo", "name": "Yahoo Mail", "host": "imap.mail.yahoo.com", "port": 993, "ssl": True,
     "note": "Requires an App Password."},
    {"id": "zoho", "name": "Zoho Mail", "host": "imap.zoho.com", "port": 993, "ssl": True, "note": ""},
    {"id": "icloud", "name": "Apple iCloud", "host": "imap.mail.me.com", "port": 993, "ssl": True,
     "note": "Requires an app-specific password."},
    {"id": "gmx", "name": "GMX", "host": "imap.gmx.com", "port": 993, "ssl": True, "note": ""},
    {"id": "cpanel", "name": "cPanel / Webmail", "host": "mail.yourdomain.com", "port": 993, "ssl": True,
     "note": "Replace host with your mail server. Port 993 (SSL) or 143 (non-SSL)."},
    {"id": "custom", "name": "Custom IMAP", "host": "", "port": 993, "ssl": True, "note": ""},
]


# ---------- Schemas ----------
class RegisterIn(BaseModel):
    email: EmailStr
    password: str = Field(min_length=6)
    name: str = "User"


class LoginIn(BaseModel):
    email: EmailStr
    password: str
    code: Optional[str] = None


class TwoFAVerify(BaseModel):
    code: str


class TwoFADisable(BaseModel):
    password: str


class TestConnectionIn(BaseModel):
    host: str
    port: int = 993
    email: str
    password: Optional[str] = None
    ssl: bool = True
    auth_method: str = "password"  # password | oauth
    access_token: Optional[str] = None


class MigrationIn(BaseModel):
    name: str
    source_host: str
    source_port: int = 993
    source_email: str
    source_password: Optional[str] = None
    source_ssl: bool = True
    source_auth_method: str = "password"
    source_oauth_provider: Optional[str] = None
    source_refresh_token: Optional[str] = None
    dest_host: str
    dest_port: int = 993
    dest_email: str
    dest_password: Optional[str] = None
    dest_ssl: bool = True
    dest_auth_method: str = "password"
    dest_oauth_provider: Optional[str] = None
    dest_refresh_token: Optional[str] = None
    scheduled_at: Optional[str] = None
    selected_folders: Optional[List[str]] = None
    recurring: bool = False
    recurring_minutes: Optional[int] = None
    date_from: Optional[str] = None
    date_to: Optional[str] = None


class ListFoldersIn(BaseModel):
    host: str
    port: int = 993
    email: str
    password: Optional[str] = None
    ssl: bool = True
    auth_method: str = "password"
    access_token: Optional[str] = None


class PackageIn(BaseModel):
    name: str
    description: Optional[str] = ""
    price: int = 0                    # paise
    max_migrations: int = 10
    storage_per_mailbox_gb: int = 50
    storage_mb: Optional[int] = None  # precise value in MB — set this for sub-1GB plans
    max_mailboxes: int = -1
    validity_days: int = 30
    is_active: bool = True
    sort_order: int = 0


class CheckoutOrderIn(BaseModel):
    package_id: str
    coupon_code: Optional[str] = None


class ValidateCouponIn(BaseModel):
    code: str
    package_id: str


class CouponIn(BaseModel):
    code: str
    percent_off: int = 10
    active: bool = True
    max_redemptions: Optional[int] = None
    expires_at: Optional[str] = None


class CheckoutVerifyIn(BaseModel):
    razorpay_order_id: str
    razorpay_payment_id: str
    razorpay_signature: str


class SettingsIn(BaseModel):
    site_name: Optional[str] = None
    tagline: Optional[str] = None
    logo_url: Optional[str] = None
    brand_color: Optional[str] = None
    hero_title: Optional[str] = None
    hero_subtitle: Optional[str] = None
    pricing_title: Optional[str] = None
    pricing_subtitle: Optional[str] = None
    contact_email: Optional[str] = None
    footer_text: Optional[str] = None


class BulkIn(BaseModel):
    migrations: List[MigrationIn]
    auto_start: bool = True


# ---------- Serializers ----------
def user_dict(u: User):
    return {"id": u.id, "email": u.email, "name": u.name, "role": u.role,
            "totp_enabled": bool(u.totp_enabled),
            "is_active": bool(u.is_active) if u.is_active is not None else True,
            "avatar_color": u.avatar_color,
            "created_at": u.created_at.isoformat() if u.created_at else None}


def mig_dict(m: Migration):
    return {
        "id": m.id, "name": m.name, "status": m.status,
        "source_host": m.source_host, "source_port": m.source_port,
        "source_email": m.source_email, "source_ssl": m.source_ssl,
        "dest_host": m.dest_host, "dest_port": m.dest_port,
        "dest_email": m.dest_email, "dest_ssl": m.dest_ssl,
        "total_emails": m.total_emails or 0, "migrated_emails": m.migrated_emails or 0,
        "failed_emails": m.failed_emails or 0,
        "total_folders": m.total_folders or 0, "migrated_folders": m.migrated_folders or 0,
        "bytes_transferred": int(m.bytes_transferred or 0),
        "error": m.error,
        "scheduled_at": m.scheduled_at.isoformat() if m.scheduled_at else None,
        "selected_folders": json.loads(m.selected_folders) if m.selected_folders else None,
        "recurring": bool(m.recurring),
        "recurring_minutes": m.recurring_minutes,
        "date_from": m.date_from.date().isoformat() if m.date_from else None,
        "date_to": m.date_to.date().isoformat() if m.date_to else None,
        "next_run_at": m.next_run_at.isoformat() if m.next_run_at else None,
        "created_at": m.created_at.isoformat() if m.created_at else None,
        "started_at": m.started_at.isoformat() if m.started_at else None,
        "completed_at": m.completed_at.isoformat() if m.completed_at else None,
    }


# DB-backed brute-force lockout (survives restarts)
MAX_ATTEMPTS = 5
LOCKOUT_MINUTES = 15


def _now():
    return datetime.now(timezone.utc).replace(tzinfo=None)


def _bf_check(db, identifier: str):
    rec = db.query(LoginLockout).filter(LoginLockout.identifier == identifier).first()
    if rec and rec.locked_until and rec.locked_until > _now():
        wait = int((rec.locked_until - _now()).total_seconds() / 60) + 1
        raise HTTPException(status_code=429, detail=f"Too many failed attempts. Try again in ~{wait} min.")


def _bf_fail(db, identifier: str):
    rec = db.query(LoginLockout).filter(LoginLockout.identifier == identifier).first()
    if not rec:
        rec = LoginLockout(identifier=identifier, fail_count=0)
        db.add(rec)
    rec.fail_count = (rec.fail_count or 0) + 1
    rec.updated_at = _now()
    if rec.fail_count >= MAX_ATTEMPTS:
        rec.locked_until = _now() + timedelta(minutes=LOCKOUT_MINUTES)
        rec.fail_count = 0
    db.commit()


def _bf_clear(db, identifier: str):
    db.query(LoginLockout).filter(LoginLockout.identifier == identifier).delete()
    db.commit()


def _parse_dt(value):
    if not value:
        return None
    try:
        s = value.replace("Z", "+00:00")
        dt = datetime.fromisoformat(s)
        if dt.tzinfo:
            dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
        return dt
    except Exception:
        return None


def pkg_dict(p: Package):
    return {"id": p.id, "name": p.name, "description": p.description, "price": p.price,
            "currency": p.currency, "max_migrations": p.max_migrations,
            "storage_per_mailbox_gb": p.storage_per_mailbox_gb, "storage_mb": p.storage_mb,
            "max_mailboxes": p.max_mailboxes,
            "validity_days": p.validity_days, "is_active": bool(p.is_active), "sort_order": p.sort_order}


def sub_dict(s: Subscription):
    active = s.status == "active" and s.expires_at and s.expires_at > _now()
    return {"id": s.id, "package_id": s.package_id, "package_name": s.package_name,
            "status": s.status, "is_active": bool(active),
            "migrations_used": s.migrations_used, "max_migrations": s.max_migrations,
            "max_mailboxes": s.max_mailboxes, "storage_per_mailbox_gb": s.storage_per_mailbox_gb,
            "storage_mb": s.storage_mb,
            "amount": s.amount,
            "starts_at": s.starts_at.isoformat() if s.starts_at else None,
            "expires_at": s.expires_at.isoformat() if s.expires_at else None,
            "created_at": s.created_at.isoformat() if s.created_at else None}


def settings_dict(s: SiteSetting):
    return {"site_name": s.site_name, "tagline": s.tagline, "logo_url": s.logo_url,
            "brand_color": s.brand_color, "hero_title": s.hero_title, "hero_subtitle": s.hero_subtitle,
            "pricing_title": s.pricing_title, "pricing_subtitle": s.pricing_subtitle,
            "contact_email": s.contact_email, "footer_text": s.footer_text}


def require_admin(current: User = Depends(get_current_user)) -> User:
    if current.role != "admin":
        raise HTTPException(status_code=403, detail="Admin access required")
    return current


def get_active_subscription(db, user_id):
    sub = (db.query(Subscription)
           .filter(Subscription.user_id == user_id, Subscription.status == "active")
           .order_by(Subscription.created_at.desc()).first())
    if sub and sub.expires_at and sub.expires_at <= _now():
        sub.status = "expired"
        db.commit()
        return None
    return sub


# ---------- Health ----------
@api.get("/")
def root():
    return {"message": "MailShift API running"}


# ---------- Auth ----------
def _email_ctx(db, user=None, **extra):
    site = settings_dict(_get_settings(db))
    ctx = {
        "site_name": site.get("site_name") or "MailShift",
        "brand_color": site.get("brand_color") or "#2563EB",
        "app_url": settings_store.get("OAUTH_PUBLIC_BASE", "") or "",
        "name": (user.name if user else "") or "there",
        "email": user.email if user else "",
    }
    ctx.update(extra)
    return ctx


@api.post("/auth/register")
def register(body: RegisterIn, db: Session = Depends(get_db)):
    email = body.email.lower().strip()
    if db.query(User).filter(User.email == email).first():
        raise HTTPException(status_code=400, detail="Email already registered")
    user = User(email=email, password_hash=hash_password(body.password), name=body.name or "User", role="user")
    db.add(user)
    db.commit()
    db.refresh(user)
    token = create_access_token(user.id, user.email)
    try:
        notifications.send_templated(db, "welcome", user.email, _email_ctx(db, user))
    except Exception:
        pass
    return {"token": token, "user": user_dict(user)}


@api.post("/auth/login")
def login(body: LoginIn, request: Request, db: Session = Depends(get_db)):
    email = body.email.lower().strip()
    xff = request.headers.get("x-forwarded-for")
    ip = xff.split(",")[0].strip() if xff else (request.client.host if request.client else "unknown")
    identifier = f"{ip}:{email}"
    _bf_check(db, identifier)
    user = db.query(User).filter(User.email == email).first()
    if not user or not verify_password(body.password, user.password_hash):
        _bf_fail(db, identifier)
        raise HTTPException(status_code=401, detail="Invalid email or password")
    if user.is_active is False:
        raise HTTPException(status_code=403, detail="This account has been disabled. Contact support.")
    if user.totp_enabled:
        if not body.code:
            return {"twofa_required": True}
        if not verify_totp(user.totp_secret, body.code):
            _bf_fail(db, identifier)
            raise HTTPException(status_code=401, detail="Invalid 2FA code")
    _bf_clear(db, identifier)
    token = create_access_token(user.id, user.email)
    return {"token": token, "user": user_dict(user)}


# ---------- Two-Factor Auth (TOTP) ----------
@api.post("/auth/2fa/setup")
def twofa_setup(current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    if current.totp_enabled:
        raise HTTPException(status_code=400, detail="2FA already enabled")
    secret = generate_totp_secret()
    current.totp_secret = secret
    db.commit()
    uri = totp_provisioning_uri(secret, current.email)
    return {"secret": secret, "otpauth_uri": uri, "qr": qr_data_url(uri)}


@api.post("/auth/2fa/enable")
def twofa_enable(body: TwoFAVerify, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    if not current.totp_secret:
        raise HTTPException(status_code=400, detail="Start 2FA setup first")
    if not verify_totp(current.totp_secret, body.code):
        raise HTTPException(status_code=400, detail="Invalid code, try again")
    current.totp_enabled = True
    db.commit()
    return {"ok": True, "totp_enabled": True}


@api.post("/auth/2fa/disable")
def twofa_disable(body: TwoFADisable, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    if not verify_password(body.password, current.password_hash):
        raise HTTPException(status_code=401, detail="Wrong password")
    current.totp_enabled = False
    current.totp_secret = None
    db.commit()
    return {"ok": True, "totp_enabled": False}


@api.get("/auth/me")
def me(current: User = Depends(get_current_user)):
    return user_dict(current)


AVATAR_COLORS = ["#2563EB", "#7C3AED", "#DB2777", "#DC2626", "#EA580C",
                  "#CA8A04", "#16A34A", "#0D9488", "#0891B2", "#4F46E5"]


class UpdateProfileIn(BaseModel):
    name: Optional[str] = None
    avatar_color: Optional[str] = None


@api.put("/auth/profile")
def update_profile(body: UpdateProfileIn, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    if body.name is not None:
        name = body.name.strip()
        if not name:
            raise HTTPException(status_code=400, detail="Name can't be empty")
        current.name = name
    if body.avatar_color is not None:
        if body.avatar_color not in AVATAR_COLORS:
            raise HTTPException(status_code=400, detail="Invalid avatar color")
        current.avatar_color = body.avatar_color
    db.commit()
    return user_dict(current)


class ChangeEmailIn(BaseModel):
    new_email: str
    password: str


@api.post("/auth/change-email")
def change_email(body: ChangeEmailIn, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    if not verify_password(body.password, current.password_hash):
        raise HTTPException(status_code=401, detail="Password is incorrect")
    new_email = body.new_email.lower().strip()
    if "@" not in new_email or "." not in new_email.split("@")[-1]:
        raise HTTPException(status_code=400, detail="Enter a valid email address")
    if db.query(User).filter(User.email == new_email, User.id != current.id).first():
        raise HTTPException(status_code=400, detail="That email is already in use")
    current.email = new_email
    db.commit()
    return user_dict(current)


class ChangePasswordIn(BaseModel):
    current_password: str
    new_password: str


@api.post("/auth/change-password")
def change_password(body: ChangePasswordIn, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    if not verify_password(body.current_password, current.password_hash):
        raise HTTPException(status_code=401, detail="Current password is incorrect")
    if len(body.new_password) < 8:
        raise HTTPException(status_code=400, detail="New password must be at least 8 characters")
    current.password_hash = hash_password(body.new_password)
    db.commit()
    try:
        notifications.send_templated(db, "password_changed", current.email, _email_ctx(db, current))
    except Exception:
        pass
    return {"ok": True}


class ForgotPasswordIn(BaseModel):
    email: str


@api.post("/auth/forgot-password")
def forgot_password(body: ForgotPasswordIn, db: Session = Depends(get_db)):
    import secrets as _secrets
    email = body.email.lower().strip()
    user = db.query(User).filter(User.email == email).first()
    # Always return ok — don't reveal whether an account exists for this email.
    if not user:
        return {"ok": True}
    token = _secrets.token_urlsafe(32)
    db.add(PasswordResetToken(token=token, user_id=user.id,
                              expires_at=_now() + timedelta(hours=1)))
    db.commit()
    reset_link = f"{(settings_store.get('OAUTH_PUBLIC_BASE', '') or '').rstrip('/')}/reset-password?token={token}"
    try:
        notifications.send_templated(db, "forgot_password", user.email,
                                     _email_ctx(db, user, reset_link=reset_link))
    except Exception:
        pass
    return {"ok": True}


class ResetPasswordIn(BaseModel):
    token: str
    new_password: str


@api.post("/auth/reset-password")
def reset_password(body: ResetPasswordIn, db: Session = Depends(get_db)):
    if len(body.new_password) < 8:
        raise HTTPException(status_code=400, detail="Password must be at least 8 characters")
    row = db.query(PasswordResetToken).filter(PasswordResetToken.token == body.token).first()
    if not row or row.used or row.expires_at < _now():
        raise HTTPException(status_code=400, detail="This reset link is invalid or has expired")
    user = db.query(User).filter(User.id == row.user_id).first()
    if not user:
        raise HTTPException(status_code=400, detail="Account not found")
    user.password_hash = hash_password(body.new_password)
    row.used = True
    db.commit()
    try:
        notifications.send_templated(db, "password_changed", user.email, _email_ctx(db, user))
    except Exception:
        pass
    return {"ok": True}


# ---------- Providers ----------
@api.get("/providers")
def providers(current: User = Depends(get_current_user)):
    return PROVIDERS


# ---------- Test Connection ----------
@api.post("/test-connection")
def test_connection(body: TestConnectionIn, current: User = Depends(get_current_user)):
    ok, msg, folders = imap_engine.test_connection(
        body.host, body.port, body.email, body.password, body.ssl,
        body.auth_method, body.access_token)
    return {"ok": ok, "message": msg, "folder_count": folders}


@api.post("/mailbox-overview")
def mailbox_overview(body: TestConnectionIn, current: User = Depends(get_current_user)):
    return imap_engine.mailbox_overview(
        body.host, body.port, body.email, body.password, body.ssl,
        body.auth_method, body.access_token)


@api.post("/list-folders")
def list_folders(body: ListFoldersIn, current: User = Depends(get_current_user)):
    ok, msg, folders = imap_engine.list_folders(
        body.host, body.port, body.email, body.password, body.ssl,
        body.auth_method, body.access_token)
    return {"ok": ok, "message": msg, "folders": folders}


# ---------- Site Settings (public branding) ----------
def _get_settings(db) -> SiteSetting:
    s = db.query(SiteSetting).filter(SiteSetting.id == "main").first()
    if not s:
        s = SiteSetting(id="main")
        db.add(s)
        db.commit()
        db.refresh(s)
    return s


@api.get("/settings")
def public_settings(db: Session = Depends(get_db)):
    return settings_dict(_get_settings(db))


@api.put("/admin/settings")
def update_settings(body: SettingsIn, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    s = _get_settings(db)
    for k, v in body.dict(exclude_unset=True).items():
        if v is not None:
            setattr(s, k, v)
    s.updated_at = _now()
    db.commit()
    return settings_dict(s)


# ---------- Packages ----------
@api.get("/packages")
def list_packages(db: Session = Depends(get_db)):
    ps = db.query(Package).filter(Package.is_active == True).order_by(Package.sort_order.asc(), Package.price.asc()).all()
    return [pkg_dict(p) for p in ps]


@api.get("/admin/packages")
def admin_list_packages(current: User = Depends(require_admin), db: Session = Depends(get_db)):
    ps = db.query(Package).order_by(Package.sort_order.asc(), Package.price.asc()).all()
    return [pkg_dict(p) for p in ps]


@api.post("/admin/packages")
def admin_create_package(body: PackageIn, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    data = body.dict()
    if not data.get("sort_order"):
        mx = db.query(Package).count()
        data["sort_order"] = mx + 1
    p = Package(**data)
    db.add(p)
    db.commit()
    db.refresh(p)
    return pkg_dict(p)


@api.put("/admin/packages/{pkg_id}")
def admin_update_package(pkg_id: str, body: PackageIn, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    p = db.query(Package).filter(Package.id == pkg_id).first()
    if not p:
        raise HTTPException(status_code=404, detail="Package not found")
    for k, v in body.dict().items():
        setattr(p, k, v)
    db.commit()
    return pkg_dict(p)


@api.delete("/admin/packages/{pkg_id}")
def admin_delete_package(pkg_id: str, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    p = db.query(Package).filter(Package.id == pkg_id).first()
    if not p:
        raise HTTPException(status_code=404, detail="Package not found")
    refs = db.query(Subscription).filter(Subscription.package_id == pkg_id).count()
    if refs > 0:
        # keep history intact: deactivate instead of hard delete
        p.is_active = False
        db.commit()
        return {"ok": True, "deactivated": True,
                "message": "Package has subscriptions; deactivated instead of deleted."}
    db.delete(p)
    db.commit()
    return {"ok": True}


# ---------- Coupons ----------
def coupon_dict(c: Coupon):
    return {"id": c.id, "code": c.code, "percent_off": c.percent_off, "active": bool(c.active),
            "max_redemptions": c.max_redemptions, "times_used": c.times_used,
            "expires_at": c.expires_at.isoformat() if c.expires_at else None}


def _validate_coupon(db, code):
    if not code:
        return None, None
    c = db.query(Coupon).filter(Coupon.code == code.strip().upper()).first()
    if not c or not c.active:
        return None, "Invalid coupon code"
    if c.expires_at and c.expires_at <= _now():
        return None, "Coupon expired"
    if c.max_redemptions is not None and (c.times_used or 0) >= c.max_redemptions:
        return None, "Coupon limit reached"
    return c, None


@api.get("/admin/coupons")
def admin_list_coupons(current: User = Depends(require_admin), db: Session = Depends(get_db)):
    out = []
    for c in db.query(Coupon).order_by(Coupon.created_at.desc()).all():
        paid = db.query(Subscription).filter(Subscription.coupon_code == c.code,
                                             Subscription.razorpay_payment_id != None).all()
        revenue = sum(int(s.amount or 0) for s in paid)
        d = coupon_dict(c)
        d["redemptions"] = len(paid)
        d["revenue_paise"] = revenue
        out.append(d)
    return out


@api.post("/admin/coupons")
def admin_create_coupon(body: CouponIn, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    code = body.code.strip().upper()
    if db.query(Coupon).filter(Coupon.code == code).first():
        raise HTTPException(status_code=400, detail="Coupon code already exists")
    pct = max(1, min(100, int(body.percent_off)))
    c = Coupon(code=code, percent_off=pct, active=body.active,
               max_redemptions=body.max_redemptions, expires_at=_parse_dt(body.expires_at))
    db.add(c)
    db.commit()
    db.refresh(c)
    return coupon_dict(c)


@api.put("/admin/coupons/{cid}")
def admin_update_coupon(cid: str, body: CouponIn, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    c = db.query(Coupon).filter(Coupon.id == cid).first()
    if not c:
        raise HTTPException(status_code=404, detail="Coupon not found")
    c.code = body.code.strip().upper()
    c.percent_off = max(1, min(100, int(body.percent_off)))
    c.active = body.active
    c.max_redemptions = body.max_redemptions
    c.expires_at = _parse_dt(body.expires_at)
    db.commit()
    return coupon_dict(c)


@api.delete("/admin/coupons/{cid}")
def admin_delete_coupon(cid: str, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    db.query(Coupon).filter(Coupon.id == cid).delete()
    db.commit()
    return {"ok": True}


@api.post("/checkout/validate-coupon")
def validate_coupon(body: ValidateCouponIn, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    pkg = db.query(Package).filter(Package.id == body.package_id).first()
    if not pkg:
        raise HTTPException(status_code=404, detail="Package not found")
    c, err = _validate_coupon(db, body.code)
    if err:
        raise HTTPException(status_code=400, detail=err)
    discount = int(round(pkg.price * c.percent_off / 100))
    return {"valid": True, "percent_off": c.percent_off, "discount": discount,
            "final_amount": max(0, pkg.price - discount), "code": c.code}


# ---------- Subscription & Checkout ----------
@api.get("/me/subscription")
def my_subscription(current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    if current.role == "admin":
        return {"is_admin": True, "subscription": None}
    sub = get_active_subscription(db, current.id)
    return {"is_admin": False, "subscription": sub_dict(sub) if sub else None}


@api.get("/payment-config")
def payment_config(current: User = Depends(get_current_user)):
    return {"configured": billing.is_configured(), "key_id": billing.key_id()}


@api.post("/checkout/order")
def checkout_order(body: CheckoutOrderIn, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    if current.role == "admin":
        raise HTTPException(status_code=400, detail="Admins have unlimited access and don't need a plan")
    pkg = db.query(Package).filter(Package.id == body.package_id, Package.is_active == True).first()
    if not pkg:
        raise HTTPException(status_code=404, detail="Package not found")
    if not billing.is_configured():
        raise HTTPException(status_code=400, detail="Payment gateway not configured")
    amount = pkg.price
    coupon_code = None
    if body.coupon_code:
        c, err = _validate_coupon(db, body.coupon_code)
        if err:
            raise HTTPException(status_code=400, detail=err)
        amount = max(100, pkg.price - int(round(pkg.price * c.percent_off / 100)))
        coupon_code = c.code
    try:
        order = billing.create_order(amount, f"ms_{current.id[:8]}_{pkg.id[:8]}")
    except Exception as e:
        raise HTTPException(status_code=502, detail=f"Razorpay error: {e}")
    sub = Subscription(user_id=current.id, package_id=pkg.id, package_name=pkg.name,
                       status="pending", max_migrations=pkg.max_migrations,
                       max_mailboxes=pkg.max_mailboxes, storage_per_mailbox_gb=pkg.storage_per_mailbox_gb,
                       storage_mb=pkg.storage_mb,
                       amount=amount, coupon_code=coupon_code, razorpay_order_id=order["id"])
    db.add(sub)
    db.commit()
    return {"order_id": order["id"], "amount": order["amount"], "currency": order["currency"],
            "key_id": billing.key_id(), "package_name": pkg.name}


@api.post("/checkout/verify")
def checkout_verify(body: CheckoutVerifyIn, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    if not billing.verify_signature(body.razorpay_order_id, body.razorpay_payment_id, body.razorpay_signature):
        raise HTTPException(status_code=400, detail="Payment verification failed")
    sub = (db.query(Subscription)
           .filter(Subscription.razorpay_order_id == body.razorpay_order_id,
                   Subscription.user_id == current.id).first())
    if not sub:
        raise HTTPException(status_code=404, detail="Order not found")
    pkg = db.query(Package).filter(Package.id == sub.package_id).first()
    validity = pkg.validity_days if pkg else 30
    sub.status = "active"
    sub.razorpay_payment_id = body.razorpay_payment_id
    sub.starts_at = _now()
    sub.expires_at = _now() + timedelta(days=validity)
    if sub.coupon_code:
        c = db.query(Coupon).filter(Coupon.code == sub.coupon_code).first()
        if c:
            c.times_used = (c.times_used or 0) + 1
    # supersede other active subs
    (db.query(Subscription)
     .filter(Subscription.user_id == current.id, Subscription.status == "active", Subscription.id != sub.id)
     .update({"status": "expired"}))
    db.commit()
    try:
        notifications.send_templated(db, "plan_renewed", current.email, _email_ctx(
            db, current, plan_name=sub.package_name, expires_on=sub.expires_at.strftime("%d %b %Y")))
    except Exception:
        pass
    return {"ok": True, "subscription": sub_dict(sub)}


@api.post("/checkout/free")
def checkout_free(body: CheckoutOrderIn, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    if current.role == "admin":
        raise HTTPException(status_code=400, detail="Admins already have unlimited access")
    pkg = db.query(Package).filter(Package.id == body.package_id, Package.is_active == True).first()
    if not pkg:
        raise HTTPException(status_code=404, detail="Package not found")
    if pkg.price != 0:
        raise HTTPException(status_code=400, detail="This is not a free plan")
    if db.query(Subscription).filter(Subscription.user_id == current.id,
                                     Subscription.package_id == pkg.id).first():
        raise HTTPException(status_code=400, detail="You have already used the free plan")
    (db.query(Subscription)
     .filter(Subscription.user_id == current.id, Subscription.status == "active")
     .update({"status": "expired"}))
    sub = Subscription(user_id=current.id, package_id=pkg.id, package_name=pkg.name,
                       status="active", max_migrations=pkg.max_migrations, max_mailboxes=pkg.max_mailboxes,
                       storage_per_mailbox_gb=pkg.storage_per_mailbox_gb, storage_mb=pkg.storage_mb, amount=0,
                       razorpay_payment_id="free", starts_at=_now(),
                       expires_at=_now() + timedelta(days=pkg.validity_days))
    db.add(sub)
    db.commit()
    try:
        notifications.send_templated(db, "plan_renewed", current.email, _email_ctx(
            db, current, plan_name=sub.package_name, expires_on=sub.expires_at.strftime("%d %b %Y")))
    except Exception:
        pass
    return {"ok": True, "subscription": sub_dict(sub)}


@api.post("/webhook/razorpay")
async def razorpay_webhook(request: Request, db: Session = Depends(get_db)):
    raw = await request.body()
    sig = request.headers.get("x-razorpay-signature", "")
    secret = settings_store.get("RAZORPAY_WEBHOOK_SECRET", "") or ""
    if not billing.verify_webhook_signature(raw, sig, secret):
        raise HTTPException(status_code=400, detail="Invalid webhook signature")
    try:
        payload = json.loads(raw.decode("utf-8"))
    except Exception:
        raise HTTPException(status_code=400, detail="Bad payload")
    event = payload.get("event", "")
    entity = (payload.get("payload", {}).get("payment", {}).get("entity", {})
              or payload.get("payload", {}).get("order", {}).get("entity", {}))
    order_id = entity.get("order_id") or entity.get("id")
    payment_id = entity.get("id") if event.startswith("payment") else None
    if event in ("payment.captured", "order.paid") and order_id:
        sub = db.query(Subscription).filter(Subscription.razorpay_order_id == order_id).first()
        if sub and sub.status != "active":
            pkg = db.query(Package).filter(Package.id == sub.package_id).first()
            sub.status = "active"
            sub.razorpay_payment_id = payment_id or sub.razorpay_payment_id or "webhook"
            sub.starts_at = _now()
            sub.expires_at = _now() + timedelta(days=(pkg.validity_days if pkg else 30))
            if sub.coupon_code:
                c = db.query(Coupon).filter(Coupon.code == sub.coupon_code).first()
                if c:
                    c.times_used = (c.times_used or 0) + 1
            db.commit()
            user = db.query(User).filter(User.id == sub.user_id).first()
            if user:
                try:
                    notifications.send_templated(db, "plan_renewed", user.email, _email_ctx(
                        db, user, plan_name=sub.package_name, expires_on=sub.expires_at.strftime("%d %b %Y")))
                except Exception:
                    pass
    return {"ok": True}


# ---------- Admin ----------
@api.get("/admin/stats")
def admin_stats(current: User = Depends(require_admin), db: Session = Depends(get_db)):
    users = db.query(User).count()
    migs = db.query(Migration).count()
    active_subs = db.query(Subscription).filter(Subscription.status == "active").count()
    paid = db.query(Subscription).filter(Subscription.status.in_(["active", "expired"]),
                                         Subscription.razorpay_payment_id != None).all()
    revenue = sum(int(s.amount or 0) for s in paid)
    return {"users": users, "migrations": migs, "active_subscriptions": active_subs,
            "revenue_paise": revenue, "packages": db.query(Package).count()}


@api.get("/admin/users")
def admin_users(current: User = Depends(require_admin), db: Session = Depends(get_db)):
    users = db.query(User).order_by(User.created_at.desc()).all()
    out = []
    for u in users:
        sub = get_active_subscription(db, u.id)
        mig_count = db.query(Migration).filter(Migration.user_id == u.id).count()
        out.append({**user_dict(u), "migrations": mig_count,
                    "plan": sub.package_name if sub else None,
                    "plan_expires": sub.expires_at.isoformat() if sub and sub.expires_at else None})
    return out


class AdminResetPasswordIn(BaseModel):
    new_password: str


@api.post("/admin/users/{user_id}/reset-password")
def admin_reset_password(user_id: str, body: AdminResetPasswordIn, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    if len(body.new_password) < 8:
        raise HTTPException(status_code=400, detail="Password must be at least 8 characters")
    u = db.query(User).filter(User.id == user_id).first()
    if not u:
        raise HTTPException(status_code=404, detail="User not found")
    u.password_hash = hash_password(body.new_password)
    db.commit()
    return {"ok": True}


class AdminSetActiveIn(BaseModel):
    is_active: bool


@api.post("/admin/users/{user_id}/set-active")
def admin_set_active(user_id: str, body: AdminSetActiveIn, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    if user_id == current.id and not body.is_active:
        raise HTTPException(status_code=400, detail="You can't disable your own account")
    u = db.query(User).filter(User.id == user_id).first()
    if not u:
        raise HTTPException(status_code=404, detail="User not found")
    u.is_active = body.is_active
    db.commit()
    if not body.is_active:
        try:
            notifications.send_templated(db, "account_disabled", u.email, _email_ctx(db, u))
        except Exception:
            pass
    return {"ok": True, "user": user_dict(u)}


class AdminUpdateUserIn(BaseModel):
    name: Optional[str] = None
    role: Optional[str] = None


@api.put("/admin/users/{user_id}")
def admin_update_user(user_id: str, body: AdminUpdateUserIn, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    u = db.query(User).filter(User.id == user_id).first()
    if not u:
        raise HTTPException(status_code=404, detail="User not found")
    if body.name is not None:
        u.name = body.name.strip() or u.name
    if body.role is not None:
        if body.role not in ("user", "admin"):
            raise HTTPException(status_code=400, detail="Role must be 'user' or 'admin'")
        if user_id == current.id and body.role != "admin":
            raise HTTPException(status_code=400, detail="You can't remove your own admin role")
        u.role = body.role
    db.commit()
    return user_dict(u)


@api.get("/admin/subscriptions")
def admin_subscriptions(current: User = Depends(require_admin), db: Session = Depends(get_db)):
    subs = (db.query(Subscription)
            .filter(Subscription.status.in_(["active", "expired"]))
            .order_by(Subscription.created_at.desc()).limit(500).all())
    users = {u.id: u.email for u in db.query(User).all()}
    return [{**sub_dict(s), "user_email": users.get(s.user_id),
             "razorpay_payment_id": s.razorpay_payment_id} for s in subs]


class IntegrationsIn(BaseModel):
    GOOGLE_CLIENT_ID: Optional[str] = None
    GOOGLE_CLIENT_SECRET: Optional[str] = None
    MS_CLIENT_ID: Optional[str] = None
    MS_CLIENT_SECRET: Optional[str] = None
    MS_TENANT: Optional[str] = None
    OAUTH_PUBLIC_BASE: Optional[str] = None
    RAZORPAY_KEY_ID: Optional[str] = None
    RAZORPAY_KEY_SECRET: Optional[str] = None
    RAZORPAY_WEBHOOK_SECRET: Optional[str] = None
    SMTP_HOST: Optional[str] = None
    SMTP_PORT: Optional[str] = None
    SMTP_USER: Optional[str] = None
    SMTP_PASSWORD: Optional[str] = None
    SMTP_FROM: Optional[str] = None
    SMTP_FROM_NAME: Optional[str] = None


@api.get("/admin/integrations")
def admin_get_integrations(current: User = Depends(require_admin)):
    view = settings_store.public_view()
    return {
        **view,
        "status": {
            "google": cloud.is_configured("google"),
            "microsoft": cloud.is_configured("microsoft"),
            "razorpay": billing.is_configured(),
            "smtp": notifications.is_configured(),
        },
        "redirect_uris": {
            "google": cloud.redirect_uri("google"),
            "microsoft": cloud.redirect_uri("microsoft"),
        },
        "webhook_url": f"{(settings_store.get('OAUTH_PUBLIC_BASE', '') or '').rstrip('/')}/api/webhook/razorpay",
    }


@api.put("/admin/integrations")
def admin_update_integrations(body: IntegrationsIn, current: User = Depends(require_admin)):
    provided = {k: v for k, v in body.dict().items() if v is not None}
    settings_store.set_many(provided)
    return admin_get_integrations(current)


class TestEmailIn(BaseModel):
    to: str


@api.post("/admin/test-email")
def admin_test_email(body: TestEmailIn, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    if not notifications.is_configured():
        raise HTTPException(status_code=400, detail="SMTP isn't configured yet — fill in the SMTP fields and save first.")
    ok = notifications.send_email(
        body.to, "MailShift test email",
        "This is a test email from your MailShift SMTP configuration. If you received this, it's working!",
    )
    if not ok:
        raise HTTPException(status_code=400, detail="Send failed — check your SMTP host/port/credentials and server logs.")
    return {"ok": True}


def _template_dict(t: EmailTemplate):
    return {"key": t.key, "name": t.name, "description": t.description, "subject": t.subject,
            "body_text": t.body_text, "body_html": t.body_html, "is_active": bool(t.is_active)}


@api.get("/admin/email-templates")
def admin_list_templates(current: User = Depends(require_admin), db: Session = Depends(get_db)):
    return [_template_dict(t) for t in db.query(EmailTemplate).order_by(EmailTemplate.name).all()]


class EmailTemplateIn(BaseModel):
    subject: str
    body_text: str
    body_html: Optional[str] = None
    is_active: bool = True


@api.put("/admin/email-templates/{key}")
def admin_update_template(key: str, body: EmailTemplateIn, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    t = db.query(EmailTemplate).filter(EmailTemplate.key == key).first()
    if not t:
        raise HTTPException(status_code=404, detail="Template not found")
    t.subject = body.subject
    t.body_text = body.body_text
    t.body_html = body.body_html
    t.is_active = body.is_active
    db.commit()
    return _template_dict(t)


@api.post("/admin/email-templates/{key}/test")
def admin_test_template(key: str, body: TestEmailIn, current: User = Depends(require_admin), db: Session = Depends(get_db)):
    if not notifications.is_configured():
        raise HTTPException(status_code=400, detail="SMTP isn't configured yet.")
    ctx = _email_ctx(db, current, plan_name="Professional", expires_on="01 Jan 2027",
                     migration_name="Sample Migration", migrated=100, total=100,
                     error="Sample error message", reset_link=f"{settings_store.get('OAUTH_PUBLIC_BASE', '') or ''}/reset-password?token=sample")
    ok = notifications.send_templated(db, key, body.to, ctx)
    if not ok:
        raise HTTPException(status_code=400, detail="Send failed — check the template is active and SMTP is configured.")
    return {"ok": True}



# ---------- Stats ----------
@api.get("/stats")
def stats(current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    q = db.query(Migration).filter(Migration.user_id == current.id)
    all_m = q.all()
    total_bytes = sum(int(m.bytes_transferred or 0) for m in all_m)
    total_emails = sum(int(m.migrated_emails or 0) for m in all_m)
    return {
        "total": len(all_m),
        "active": sum(1 for m in all_m if m.status in ("running", "pending", "paused", "queued", "scheduled")),
        "running": sum(1 for m in all_m if m.status == "running"),
        "scheduled": sum(1 for m in all_m if m.status == "scheduled"),
        "completed": sum(1 for m in all_m if m.status == "completed"),
        "failed": sum(1 for m in all_m if m.status == "failed"),
        "bytes_transferred": total_bytes,
        "emails_migrated": total_emails,
    }


# ---------- Migrations ----------
def _get_owned(mig_id: str, current: User, db: Session) -> Migration:
    m = db.query(Migration).filter(Migration.id == mig_id, Migration.user_id == current.id).first()
    if not m:
        raise HTTPException(status_code=404, detail="Migration not found")
    return m


def _enforce_quota(db, current, count=1):
    if current.role == "admin":
        return
    sub = get_active_subscription(db, current.id)
    if not sub:
        raise HTTPException(status_code=402, detail="No active plan. Please purchase a package to create migrations.")
    if sub.max_migrations != -1 and (sub.migrations_used + count) > sub.max_migrations:
        remaining = max(0, sub.max_migrations - sub.migrations_used)
        raise HTTPException(status_code=402,
                            detail=f"Migration limit reached ({remaining} left of {sub.max_migrations}). Upgrade your plan.")
    sub.migrations_used = (sub.migrations_used or 0) + count
    db.commit()


def _create_migration(body: MigrationIn, current: User, db: Session) -> Migration:
    _enforce_quota(db, current, 1)
    scheduled = _parse_dt(body.scheduled_at)
    is_future = scheduled and scheduled > _now()
    recurring = bool(body.recurring and body.recurring_minutes and body.recurring_minutes > 0)

    if body.source_auth_method == "oauth" and not body.source_refresh_token:
        raise HTTPException(status_code=400, detail="Source mailbox is not connected via OAuth — click Connect first.")
    if body.dest_auth_method == "oauth" and not body.dest_refresh_token:
        raise HTTPException(status_code=400, detail="Destination mailbox is not connected via OAuth — click Connect first.")
    if body.source_auth_method != "oauth" and not body.source_password:
        raise HTTPException(status_code=400, detail="Source mailbox password is required.")
    if body.dest_auth_method != "oauth" and not body.dest_password:
        raise HTTPException(status_code=400, detail="Destination mailbox password is required.")

    m = Migration(
        user_id=current.id, name=body.name,
        source_host=body.source_host.strip(), source_port=body.source_port,
        source_email=body.source_email.strip(),
        source_password_enc=encrypt_secret(body.source_password) if body.source_password else None,
        source_ssl=body.source_ssl,
        source_auth_method=body.source_auth_method,
        source_oauth_provider=body.source_oauth_provider,
        source_refresh_token_enc=encrypt_secret(body.source_refresh_token) if body.source_refresh_token else None,
        dest_host=body.dest_host.strip(), dest_port=body.dest_port,
        dest_email=body.dest_email.strip(),
        dest_password_enc=encrypt_secret(body.dest_password) if body.dest_password else None,
        dest_ssl=body.dest_ssl,
        dest_auth_method=body.dest_auth_method,
        dest_oauth_provider=body.dest_oauth_provider,
        dest_refresh_token_enc=encrypt_secret(body.dest_refresh_token) if body.dest_refresh_token else None,
        scheduled_at=scheduled if is_future else None,
        selected_folders=json.dumps(body.selected_folders) if body.selected_folders else None,
        recurring=recurring,
        recurring_minutes=body.recurring_minutes if recurring else None,
        next_run_at=(_now() + timedelta(minutes=body.recurring_minutes)) if recurring else None,
        date_from=_parse_dt(body.date_from),
        date_to=_parse_dt(body.date_to),
        status="scheduled" if is_future else "pending",
    )
    db.add(m)
    db.commit()
    db.refresh(m)
    return m


@api.get("/migrations")
def list_migrations(current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    ms = db.query(Migration).filter(Migration.user_id == current.id).order_by(Migration.created_at.desc()).all()
    return [mig_dict(m) for m in ms]


@api.post("/migrations")
def create_migration(body: MigrationIn, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    m = _create_migration(body, current, db)
    return mig_dict(m)


@api.post("/migrations/bulk")
def bulk_create(body: BulkIn, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    created = []
    for row in body.migrations:
        m = _create_migration(row, current, db)
        created.append(m.id)
        if body.auto_start and m.status != "scheduled":
            imap_engine.start_migration(m.id)
    return {"created": len(created), "ids": created}


@api.get("/migrations/{mig_id}")
def get_migration(mig_id: str, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    m = _get_owned(mig_id, current, db)
    d = mig_dict(m)
    d["is_running"] = imap_engine.is_running(mig_id)
    d["is_active"] = imap_engine.is_active(mig_id)
    return d


@api.get("/migrations/{mig_id}/folders")
def get_folders(mig_id: str, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    _get_owned(mig_id, current, db)
    rows = db.query(MigrationFolder).filter(MigrationFolder.migration_id == mig_id).order_by(MigrationFolder.name.asc()).all()
    return [{"id": r.id, "name": r.name, "total": r.total or 0, "migrated": r.migrated or 0,
             "status": r.status} for r in rows]


@api.get("/migrations/{mig_id}/logs")
def get_logs(mig_id: str, after: int = 0, current: User = Depends(get_current_user),
             db: Session = Depends(get_db)):
    _get_owned(mig_id, current, db)
    rows = (db.query(MigrationLog)
            .filter(MigrationLog.migration_id == mig_id, MigrationLog.id > after)
            .order_by(MigrationLog.id.asc()).limit(500).all())
    return [{"id": r.id, "ts": r.ts.isoformat() if r.ts else None, "level": r.level,
             "message": r.message} for r in rows]


@api.get("/migrations/{mig_id}/report")
def migration_report(mig_id: str, fmt: str = "pdf", current: User = Depends(get_current_user),
                     db: Session = Depends(get_db)):
    from fastapi.responses import Response
    m = _get_owned(mig_id, current, db)
    folders = (db.query(MigrationFolder).filter(MigrationFolder.migration_id == mig_id)
               .order_by(MigrationFolder.name.asc()).all())
    safe_name = "".join(c if c.isalnum() or c in "-_ " else "_" for c in (m.name or "migration")).strip() or "migration"
    if fmt == "csv":
        data = report.build_csv(m, folders)
        return Response(content=data, media_type="text/csv",
                        headers={"Content-Disposition": f'attachment; filename="{safe_name}-report.csv"'})
    site = settings_dict(_get_settings(db))
    data = report.build_pdf(m, folders, site)
    return Response(content=data, media_type="application/pdf",
                    headers={"Content-Disposition": f'attachment; filename="{safe_name}-report.pdf"'})



@api.post("/migrations/{mig_id}/start")
def start(mig_id: str, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    m = _get_owned(mig_id, current, db)
    if imap_engine.is_active(mig_id):
        raise HTTPException(status_code=400, detail="Migration already running or queued")
    # Re-running a completed/failed migration acts as a delta sync (dedup skips existing mail)
    if m.status == "completed":
        db.add(MigrationLog(migration_id=mig_id, level="info",
                            message="Sync run started — copying only new emails"))
        db.commit()
    imap_engine.start_migration(mig_id)
    return {"ok": True, "status": "queued"}


@api.get("/queue")
def queue_status(current: User = Depends(get_current_user)):
    queued, running, cap = imap_engine.queue_depth()
    return {"queued": queued, "running": running, "max_workers": cap}


@api.post("/migrations/{mig_id}/pause")
def pause(mig_id: str, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    m = _get_owned(mig_id, current, db)
    imap_engine.set_control(mig_id, "pause")
    m.status = "paused"
    db.commit()
    return {"ok": True, "status": "paused"}


@api.post("/migrations/{mig_id}/resume")
def resume(mig_id: str, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    m = _get_owned(mig_id, current, db)
    if imap_engine.is_running(mig_id):
        imap_engine.set_control(mig_id, "run")
        m.status = "running"
        db.commit()
    else:
        imap_engine.start_migration(mig_id)
    return {"ok": True, "status": "running"}


@api.post("/migrations/{mig_id}/cancel")
def cancel(mig_id: str, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    m = _get_owned(mig_id, current, db)
    imap_engine.set_control(mig_id, "cancel")
    if not imap_engine.is_running(mig_id):
        m.status = "failed"
        m.error = "Cancelled by user"
        db.commit()
    return {"ok": True}


@api.delete("/migrations/{mig_id}")
def delete_migration(mig_id: str, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    m = _get_owned(mig_id, current, db)
    imap_engine.set_control(mig_id, "cancel")
    db.delete(m)
    db.commit()
    return {"ok": True}


class CloudMigrationIn(BaseModel):
    name: str
    kind: str = "files"
    source_account_id: str
    dest_account_id: str


@api.get("/cloud/config")
def cloud_config(current: User = Depends(get_current_user)):
    return {"google": cloud.is_configured("google"), "microsoft": cloud.is_configured("microsoft")}


@api.get("/oauth/{provider}/authorize")
def oauth_authorize(provider: str, current: User = Depends(get_current_user)):
    if provider not in ("google", "microsoft"):
        raise HTTPException(status_code=404, detail="Unknown provider")
    if not cloud.is_configured(provider):
        raise HTTPException(status_code=400, detail=f"{provider} OAuth not configured")
    state = create_access_token(current.id, current.email)
    return {"url": cloud.auth_url(provider, state)}


# ---------- Mail OAuth (for connecting a mailbox to migrate, via popup) ----------
@api.get("/oauth/mail/{provider}/authorize")
def oauth_mail_authorize(provider: str, current: User = Depends(get_current_user)):
    if provider not in ("google", "microsoft"):
        raise HTTPException(status_code=404, detail="Unknown provider")
    if not cloud.is_configured(provider):
        raise HTTPException(status_code=400, detail=f"{provider} OAuth is not configured on the server.")
    state = create_access_token(current.id, current.email)
    return {"url": cloud.mail_auth_url(provider, state)}


@api.get("/oauth/mail/{provider}/callback")
def oauth_mail_callback(provider: str, code: str = "", state: str = "", error: str = ""):
    """Popup callback: exchanges the code, then posts the tokens back to the
    opener window (the New Migration form) and closes itself."""
    from starlette.responses import HTMLResponse

    def popup_html(payload: dict) -> str:
        delay = 400 if payload.get("status") == "connected" else 15000
        err_text = ("Connected — you can close this window." if payload.get("status") == "connected"
                    else "Error: " + str(payload.get("error")))
        return f"""<html><body style="font-family:sans-serif;padding:2rem;text-align:center">
        <p>{err_text}</p>
        <script>
          if (window.opener) {{ window.opener.postMessage({json.dumps(payload)}, "*"); }}
          setTimeout(() => window.close(), {delay});
        </script>
        </body></html>"""

    if error or not code or not state:
        return HTMLResponse(popup_html({"source": "mailshift-oauth", "provider": provider, "status": "error",
                                         "error": error or "missing code"}))
    try:
        _jwt.decode(state, os.environ["JWT_SECRET"], algorithms=["HS256"])  # validates the request came from us
        tokens = cloud.mail_exchange_code(provider, code)
        if provider == "microsoft":
            email = cloud.email_from_id_token(tokens.get("id_token", "")) or "microsoft-account"
        else:
            email = cloud.get_account_email(provider, tokens.get("access_token"))
        if not tokens.get("refresh_token"):
            # Google only returns a refresh_token on first consent; if this account
            # was already connected before, ask the user to revoke access and retry.
            return HTMLResponse(popup_html({
                "source": "mailshift-oauth", "provider": provider, "status": "error",
                "error": "No refresh token returned. Remove this app's access in your account security settings and try connecting again.",
            }))
        return HTMLResponse(popup_html({
            "source": "mailshift-oauth", "provider": provider, "status": "connected",
            "email": email, "access_token": tokens.get("access_token"),
            "refresh_token": tokens.get("refresh_token"),
        }))
    except Exception as e:
        import logging
        logging.getLogger("uvicorn.error").error(f"MAIL OAUTH CALLBACK FAILED ({provider}): {e}")
        return HTMLResponse(popup_html({"source": "mailshift-oauth", "provider": provider, "status": "error",
                                         "error": str(e)[:400]}))


@api.get("/oauth/{provider}/callback")
def oauth_callback(provider: str, code: str = "", state: str = "", db: Session = Depends(get_db)):
    base = settings_store.get("OAUTH_PUBLIC_BASE", "") or ""
    try:
        payload = _jwt.decode(state, os.environ["JWT_SECRET"], algorithms=["HS256"])
        user_id = payload["sub"]
        tokens = cloud.exchange_code(provider, code)
        cloud.save_account(db, user_id, provider, tokens)
        from starlette.responses import RedirectResponse
        return RedirectResponse(url=f"{base}/cloud?connected={provider}")
    except Exception as e:
        from starlette.responses import RedirectResponse
        return RedirectResponse(url=f"{base}/cloud?error={str(e)[:80]}")


@api.get("/cloud/accounts")
def cloud_accounts(current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    accs = db.query(CloudAccount).filter(CloudAccount.user_id == current.id).all()
    return [{"id": a.id, "provider": a.provider, "email": a.email,
             "created_at": a.created_at.isoformat() if a.created_at else None} for a in accs]


@api.delete("/cloud/accounts/{aid}")
def cloud_account_delete(aid: str, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    db.query(CloudAccount).filter(CloudAccount.id == aid, CloudAccount.user_id == current.id).delete()
    db.commit()
    return {"ok": True}


def cm_dict(m: CloudMigration):
    return {"id": m.id, "name": m.name, "kind": m.kind, "status": m.status,
            "total": m.total or 0, "migrated": m.migrated or 0, "failed": m.failed or 0,
            "bytes_transferred": int(m.bytes_transferred or 0), "logs": m.logs, "error": m.error,
            "created_at": m.created_at.isoformat() if m.created_at else None}


@api.get("/cloud/migrations")
def cloud_list(current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    ms = db.query(CloudMigration).filter(CloudMigration.user_id == current.id).order_by(CloudMigration.created_at.desc()).all()
    return [cm_dict(m) for m in ms]


@api.get("/cloud/migrations/{mid}")
def cloud_get(mid: str, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    m = db.query(CloudMigration).filter(CloudMigration.id == mid, CloudMigration.user_id == current.id).first()
    if not m:
        raise HTTPException(status_code=404, detail="Not found")
    return cm_dict(m)


@api.post("/cloud/migrations")
def cloud_create(body: CloudMigrationIn, current: User = Depends(get_current_user), db: Session = Depends(get_db)):
    _enforce_quota(db, current, 1)
    m = CloudMigration(user_id=current.id, name=body.name, kind=body.kind,
                       source_account_id=body.source_account_id, dest_account_id=body.dest_account_id,
                       status="pending")
    db.add(m)
    db.commit()
    db.refresh(m)
    cloud.start_cloud_migration(m.id)
    return cm_dict(m)


app.include_router(api)
app.add_middleware(
    CORSMiddleware,
    allow_credentials=False,
    allow_origins=os.environ.get("CORS_ORIGINS", "*").split(","),
    allow_methods=["*"],
    allow_headers=["*"],
)


def seed_admin():
    db = next(get_db())
    try:
        admin_email = os.environ.get("ADMIN_EMAIL", "admin@mailshift.com").lower()
        admin_password = os.environ.get("ADMIN_PASSWORD", "admin123")
        existing = db.query(User).filter(User.email == admin_email).first()
        if not existing:
            db.add(User(email=admin_email, password_hash=hash_password(admin_password),
                        name="Admin", role="admin"))
            db.commit()
            logger.info("Seeded admin user")
        elif not verify_password(admin_password, existing.password_hash):
            existing.password_hash = hash_password(admin_password)
            db.commit()
    finally:
        db.close()


@app.on_event("startup")
def on_startup():
    Base.metadata.create_all(bind=engine)
    seed_admin()
    seed_packages_and_settings()
    seed_email_templates()
    settings_store.refresh()
    imap_engine.start_scheduler()
    _start_plan_expiry_checker()
    logger.info("MailShift API started")


DEFAULT_EMAIL_TEMPLATES = [
    {
        "key": "welcome",
        "name": "Welcome / Registration",
        "description": "Sent right after a user registers. Variables: {{name}}, {{email}}",
        "subject": "Welcome to {{site_name}}, {{name}}!",
        "body_text": "Hi {{name}},\n\nYour {{site_name}} account is ready. You can now connect your mailboxes and start migrating.\n\nGet started: {{app_url}}\n\nThanks,\nThe {{site_name}} Team",
    },
    {
        "key": "forgot_password",
        "name": "Forgot Password",
        "description": "Sent when a user requests a password reset. Variables: {{name}}, {{reset_link}}",
        "subject": "Reset your {{site_name}} password",
        "body_text": "Hi {{name}},\n\nWe received a request to reset your password. Click the link below to choose a new one (valid for 1 hour):\n\n{{reset_link}}\n\nIf you didn't request this, you can ignore this email.\n\nThanks,\nThe {{site_name}} Team",
    },
    {
        "key": "password_changed",
        "name": "Password Changed",
        "description": "Sent after a successful password change, as a security confirmation. Variables: {{name}}",
        "subject": "Your {{site_name}} password was changed",
        "body_text": "Hi {{name}},\n\nThis confirms your account password was just changed. If this wasn't you, please contact support immediately.\n\nThanks,\nThe {{site_name}} Team",
    },
    {
        "key": "plan_expiring",
        "name": "Plan Expiring Soon",
        "description": "Sent 3 days before a subscription expires. Variables: {{name}}, {{plan_name}}, {{expires_on}}",
        "subject": "Your {{site_name}} plan expires soon",
        "body_text": "Hi {{name}},\n\nYour {{plan_name}} plan expires on {{expires_on}}. Renew now to avoid interruption to your migrations.\n\nRenew: {{app_url}}/pricing\n\nThanks,\nThe {{site_name}} Team",
    },
    {
        "key": "plan_renewed",
        "name": "Plan Renewed",
        "description": "Sent after a successful plan purchase/renewal. Variables: {{name}}, {{plan_name}}, {{expires_on}}",
        "subject": "Your {{site_name}} plan is active",
        "body_text": "Hi {{name}},\n\nYour {{plan_name}} plan is now active and valid until {{expires_on}}. Thanks for your business!\n\nThanks,\nThe {{site_name}} Team",
    },
    {
        "key": "migration_completed",
        "name": "Migration Completed",
        "description": "Sent when a migration finishes successfully. Variables: {{name}}, {{migration_name}}, {{migrated}}, {{total}}",
        "subject": "Migration completed: {{migration_name}}",
        "body_text": "Hi {{name}},\n\nYour migration '{{migration_name}}' completed successfully — {{migrated}}/{{total}} emails migrated.\n\nView details: {{app_url}}\n\nThanks,\nThe {{site_name}} Team",
    },
    {
        "key": "migration_failed",
        "name": "Migration Failed",
        "description": "Sent when a migration fails. Variables: {{name}}, {{migration_name}}, {{error}}",
        "subject": "Migration failed: {{migration_name}}",
        "body_text": "Hi {{name}},\n\nYour migration '{{migration_name}}' failed.\n\nError: {{error}}\n\nYou can retry it from your dashboard: {{app_url}}\n\nThanks,\nThe {{site_name}} Team",
    },
    {
        "key": "account_disabled",
        "name": "Account Disabled",
        "description": "Sent when an admin disables a user's account. Variables: {{name}}",
        "subject": "Your {{site_name}} account has been disabled",
        "body_text": "Hi {{name}},\n\nYour account has been disabled by an administrator. If you believe this is a mistake, please contact support.\n\nThanks,\nThe {{site_name}} Team",
    },
]


def _start_plan_expiry_checker():
    import threading
    import time as _time

    def loop():
        while True:
            try:
                db = next(get_db())
                try:
                    soon = _now() + timedelta(days=3)
                    subs = (db.query(Subscription)
                            .filter(Subscription.status == "active",
                                    Subscription.expires_at != None,
                                    Subscription.expires_at <= soon,
                                    Subscription.expires_at > _now(),
                                    Subscription.expiry_notified != True).all())
                    for sub in subs:
                        user = db.query(User).filter(User.id == sub.user_id).first()
                        if not user:
                            continue
                        try:
                            notifications.send_templated(db, "plan_expiring", user.email, _email_ctx(
                                db, user, plan_name=sub.package_name,
                                expires_on=sub.expires_at.strftime("%d %b %Y")))
                        except Exception:
                            pass
                        sub.expiry_notified = True
                    db.commit()
                finally:
                    db.close()
            except Exception:
                pass
            _time.sleep(6 * 3600)  # check every 6 hours

    threading.Thread(target=loop, daemon=True).start()


def _html_email(title, body_lines, cta_text=None, cta_url_var=None, footnote=None):
    """Builds a styled, centered card-style HTML email (inline CSS, since most email
    clients strip <style> tags). Uses {{site_name}}/{{brand_color}} template vars,
    substituted later by render_template() same as everything else."""
    paras = "".join(
        f'<p style="margin:0 0 14px;color:#475569;font-size:14px;line-height:1.6;">{line}</p>'
        for line in body_lines
    )
    button = ""
    if cta_text and cta_url_var:
        button = f'''
        <div style="text-align:center;margin:28px 0;">
          <a href="{{{{{cta_url_var}}}}}" style="background:{{{{brand_color}}}};color:#ffffff;text-decoration:none;
             font-weight:600;font-size:14px;padding:12px 28px;border-radius:8px;display:inline-block;">{cta_text}</a>
        </div>'''
    foot_box = ""
    if footnote:
        foot_box = f'''
        <div style="background:#F1F5F9;border-radius:8px;padding:16px;margin-top:24px;">
          <p style="margin:0;color:#64748B;font-size:12px;line-height:1.6;text-align:center;">{footnote}</p>
        </div>'''
    return f'''<div style="background:#F1F5F9;padding:40px 16px;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;">
  <div style="max-width:480px;margin:0 auto;background:#ffffff;border-radius:16px;padding:40px 32px;
              box-shadow:0 1px 3px rgba(0,0,0,0.08);">
    <div style="text-align:center;margin-bottom:28px;">
      <span style="color:{{{{brand_color}}}};font-size:22px;font-weight:800;letter-spacing:-0.5px;">{{{{site_name}}}}</span>
    </div>
    <h1 style="margin:0 0 16px;color:#0F172A;font-size:22px;font-weight:700;text-align:center;">{title}</h1>
    {paras}
    {button}
    {foot_box}
    <p style="margin:24px 0 0;color:#94A3B8;font-size:11px;text-align:center;">
      &copy; {{{{site_name}}}} &middot; You're receiving this email because you have an account with us.
    </p>
  </div>
</div>'''


DEFAULT_TEMPLATE_HTML = {
    "welcome": _html_email(
        "Welcome, {{name}}!",
        ["Your {{site_name}} account is ready. You can now connect your mailboxes and start migrating emails between providers."],
        cta_text="Get started", cta_url_var="app_url",
        footnote="Need help? Just reply to this email and we'll get back to you."),
    "forgot_password": _html_email(
        "Reset your password",
        ["We received a request to reset the password for {{email}}. Click below to choose a new one — this link is valid for 1 hour."],
        cta_text="Reset password", cta_url_var="reset_link",
        footnote="If you didn't request this, you can safely ignore this email — your password won't change."),
    "password_changed": _html_email(
        "Password changed",
        ["This confirms the password for your {{site_name}} account was just changed."],
        footnote="If this wasn't you, please contact support immediately to secure your account."),
    "plan_expiring": _html_email(
        "Your plan expires soon",
        ["Your <b>{{plan_name}}</b> plan expires on <b>{{expires_on}}</b>. Renew now to avoid any interruption to your migrations."],
        cta_text="Renew my plan", cta_url_var="app_url",
        footnote="Migrations already in progress won't be interrupted, but you won't be able to start new ones after expiry."),
    "plan_renewed": _html_email(
        "You're all set!",
        ["Your <b>{{plan_name}}</b> plan is now active and valid until <b>{{expires_on}}</b>. Thanks for your business!"],
        cta_text="Go to dashboard", cta_url_var="app_url"),
    "migration_completed": _html_email(
        "Migration completed",
        ["Your migration <b>'{{migration_name}}'</b> completed successfully — <b>{{migrated}}/{{total}}</b> emails migrated."],
        cta_text="View details", cta_url_var="app_url"),
    "migration_failed": _html_email(
        "Migration failed",
        ["Your migration <b>'{{migration_name}}'</b> ran into a problem and didn't complete.",
         "<b>Error:</b> {{error}}"],
        cta_text="Retry migration", cta_url_var="app_url",
        footnote="Common causes: expired credentials, a dropped connection, or a mailbox permission issue."),
    "account_disabled": _html_email(
        "Account disabled",
        ["Your {{site_name}} account has been disabled by an administrator."],
        footnote="If you believe this is a mistake, please contact support."),
}


def seed_email_templates():
    db = next(get_db())
    try:
        for t in DEFAULT_EMAIL_TEMPLATES:
            html = DEFAULT_TEMPLATE_HTML.get(t["key"])
            row = db.query(EmailTemplate).filter(EmailTemplate.key == t["key"]).first()
            if not row:
                db.add(EmailTemplate(key=t["key"], name=t["name"], description=t["description"],
                                      subject=t["subject"], body_text=t["body_text"], body_html=html, is_active=True))
            elif not row.body_html and html:
                # Upgrades installs seeded before HTML templates existed, without
                # touching anything an admin may have already customized.
                row.body_html = html
        db.commit()
    finally:
        db.close()


def seed_packages_and_settings():
    db = next(get_db())
    try:
        _get_settings(db)  # ensure settings row exists
        if db.query(Package).count() == 0:
            defaults = [
                Package(name="Starter", description="For small one-off migrations",
                        price=99900, max_migrations=10, storage_per_mailbox_gb=25,
                        max_mailboxes=10, validity_days=30, sort_order=1),
                Package(name="Professional", description="Best for agencies & bulk moves",
                        price=299900, max_migrations=20, storage_per_mailbox_gb=50,
                        max_mailboxes=20, validity_days=60, sort_order=2),
                Package(name="Business", description="High-volume, unlimited migrations",
                        price=599900, max_migrations=-1, storage_per_mailbox_gb=50,
                        max_mailboxes=-1, validity_days=90, sort_order=3),
            ]
            db.add_all(defaults)
            db.commit()
            logger.info("Seeded default packages")
        if not db.query(Package).filter(Package.name == "Free Trial").first():
            db.add(Package(name="Free Trial", description="Try one migration free",
                           price=0, max_migrations=1, storage_per_mailbox_gb=5,
                           max_mailboxes=1, validity_days=7, sort_order=0))
            db.commit()
            logger.info("Seeded Free Trial package")
    finally:
        db.close()
