from datetime import timedelta
from typing import Optional
import re

from fastapi import APIRouter, Depends, HTTPException, Request, Response
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session

from pydantic import BaseModel

from .. import config, payments, schemas, security, serialize
from ..db import IS_POSTGRES, get_db
from ..models import Category, DeliveryZone, Order, OrderItem, Payment, Product, User, now
from ..services import audit, get_setting, place_order

router = APIRouter(prefix="/api")


def _needs_setup(db: Session) -> bool:
    return bool(config.SETUP_CODE) and not db.scalar(select(User.id).where(User.role == "owner"))


@router.post("/setup")
def first_owner(body: schemas.FirstOwner, response: Response, request: Request, db: Session = Depends(get_db)):
    """Makes the very first owner. Works only while there is no owner AND the host set SETUP_CODE."""
    import hmac
    if not _needs_setup(db):
        raise HTTPException(404, "Not found.")
    security.throttle_check("setup")
    if not hmac.compare_digest(body.code.strip().encode(), config.SETUP_CODE.encode()):
        security.throttle_fail("setup")
        raise HTTPException(403, "That setup code is not right.")
    problem = security.password_problem(body.password)
    if problem:
        raise HTTPException(400, problem)
    email = body.email.strip().lower()
    if not re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", email):
        raise HTTPException(400, "Enter a valid email address.")
    if db.scalar(select(User.id).where(User.email == email)):
        raise HTTPException(409, "That email already has an account. Use a different one.")
    u = User(name=body.name.strip(), email=email, role="owner", password_hash=security.hash_password(body.password))
    db.add(u)
    audit(db, email, "first_owner_created")
    db.commit()
    security.start_session(response, u)
    return {"user": serialize.user(u)}


# ---------------------------------------------------------------- storefront

@router.get("/store")
def store(db: Session = Depends(get_db)):
    zones = db.scalars(select(DeliveryZone).where(DeliveryZone.active).order_by(DeliveryZone.fee, DeliveryZone.name))
    return {
        "name": get_setting(db, "store_name", "Abrokyire Aba Ghana"),
        "demo": config.DEMO,
        "online_payments": payments.enabled(),
        "payment_provider": config.PAYMENT_PROVIDER,
        "needs_setup": _needs_setup(db),
        "free_delivery_over": float(get_setting(db, "free_delivery_over", "250")),
        "contact_phone": get_setting(db, "contact_phone", ""),
        "momo_instructions": get_setting(db, "momo_instructions", ""),
        "zones": [{"id": z.id, "name": z.name, "fee": float(z.fee), "eta": z.eta} for z in zones],
    }


@router.get("/categories")
def categories(db: Session = Depends(get_db)):
    rows = db.execute(
        select(Category.id, Category.name, func.count(Product.id))
        .outerjoin(Product, (Product.category_id == Category.id) & Product.active)
        .group_by(Category.id).order_by(Category.sort, Category.name)).all()
    return [{"id": i, "name": n, "count": c} for i, n, c in rows]


SORTS = {
    "featured": (Product.badge == "", Product.name),
    "price_asc": (Product.price, Product.name),
    "price_desc": (Product.price.desc(), Product.name),
    "name": (Product.name,),
    "newest": (Product.created_at.desc(), Product.name),
}


def _since(days: int):
    return now() - timedelta(days=days)


def _sold_30(db: Session):
    """product_id -> units sold in the last 30 days (cancelled orders do not count)."""
    rows = db.execute(select(OrderItem.product_id, func.sum(OrderItem.quantity)).join(Order, Order.id == OrderItem.order_id)
                      .where(Order.status != "cancelled", Order.created_at >= _since(30)).group_by(OrderItem.product_id)).all()
    return {pid: int(q or 0) for pid, q in rows}


def _filtered(q: str, category: Optional[int], brand: str, min_price: Optional[float], max_price: Optional[float],
              in_stock: bool, deals: bool, skip: str = ""):
    stmt = select(Product).where(Product.active)
    if q.strip():
        like = f"%{q.strip()}%"
        stmt = stmt.where(or_(Product.name.ilike(like), Product.brand.ilike(like), Product.sku.ilike(like),
                              Product.description.ilike(like)))
    if category:
        stmt = stmt.where(Product.category_id == category)
    brands = [b.strip() for b in brand.split(",") if b.strip()]
    if brands and skip != "brand":
        stmt = stmt.where(Product.brand.in_(brands))
    if min_price is not None and skip != "price":
        stmt = stmt.where(Product.price >= min_price)
    if max_price is not None and skip != "price":
        stmt = stmt.where(Product.price <= max_price)
    if in_stock:
        stmt = stmt.where(Product.stock > 0)
    if deals:
        stmt = stmt.where(Product.badge != "")
    return stmt


@router.get("/products")
def products(q: str = "", category: Optional[int] = None, sort: str = "featured", in_stock: bool = False,
             brand: str = "", min_price: Optional[float] = None, max_price: Optional[float] = None, deals: bool = False,
             page: int = 1, per_page: int = 24, db: Session = Depends(get_db)):
    per_page = max(1, min(per_page, 60))
    stmt = _filtered(q, category, brand, min_price, max_price, in_stock, deals)
    total = db.scalar(select(func.count()).select_from(stmt.subquery()))
    if sort == "popular":
        sold = _sold_30(db)
        rows = sorted(db.scalars(stmt).all(), key=lambda p: (-sold.get(p.id, 0), p.name))
        rows = rows[(max(page, 1) - 1) * per_page:][:per_page]
    else:
        stmt = stmt.order_by(*SORTS.get(sort, SORTS["featured"])).offset((max(page, 1) - 1) * per_page).limit(per_page)
        rows = db.scalars(stmt)
    return {"total": total, "page": page, "per_page": per_page, "items": [serialize.product(p) for p in rows]}


@router.get("/facets")
def facets(q: str = "", category: Optional[int] = None, brand: str = "", in_stock: bool = False, deals: bool = False,
           db: Session = Depends(get_db)):
    """The filter choices for what the customer is looking at: brands with counts, and the price range."""
    base = _filtered(q, category, brand, None, None, in_stock, deals, skip="brand").subquery()
    brands = db.execute(select(base.c.brand, func.count()).where(base.c.brand != "").group_by(base.c.brand)
                        .order_by(func.count().desc(), base.c.brand).limit(40)).all()
    lo, hi = db.execute(select(func.min(base.c.price), func.max(base.c.price))).one()
    return {"brands": [{"name": n, "count": c} for n, c in brands],
            "min_price": float(lo) if lo is not None else 0, "max_price": float(hi) if hi is not None else 0}


@router.get("/suggest")
def suggest(q: str = "", db: Session = Depends(get_db)):
    """Search-as-you-type: a few products, brands and departments."""
    q = q.strip()
    if len(q) < 2:
        return {"products": [], "brands": [], "categories": []}
    like = f"%{q}%"
    prods = db.scalars(select(Product).where(Product.active, or_(Product.name.ilike(like), Product.brand.ilike(like)))
                       .order_by(Product.stock == 0, Product.name).limit(6)).all()
    brands = db.scalars(select(Product.brand).where(Product.active, Product.brand.ilike(like), Product.brand != "")
                        .group_by(Product.brand).order_by(Product.brand).limit(3)).all()
    cats = db.execute(select(Category.id, Category.name).where(Category.name.ilike(like)).limit(3)).all()
    return {"products": [serialize.product(p) for p in prods], "brands": list(brands),
            "categories": [{"id": i, "name": n} for i, n in cats]}


@router.get("/products/by-ids")
def products_by_ids(ids: str = "", db: Session = Depends(get_db)):
    """Saved items, recently viewed and 'order again' all ask for products by id. Hidden ones are skipped."""
    want = [int(x) for x in ids.split(",") if x.strip().isdigit()][:40]
    found = {p.id: p for p in db.scalars(select(Product).where(Product.id.in_(want), Product.active))} if want else {}
    return [serialize.product(found[i]) for i in want if i in found]


@router.get("/home")
def home_rows(db: Session = Depends(get_db)):
    sold = _sold_30(db)
    items = db.scalars(select(Product).where(Product.active, Product.stock > 0)).all()
    popular = sorted(items, key=lambda p: (-sold.get(p.id, 0), p.name))[:10]
    deals = [p for p in items if p.badge][:10]
    return {"popular": [serialize.product(p) for p in popular if sold.get(p.id)],
            "deals": [serialize.product(p) for p in deals]}


@router.get("/products/{pid}")
def product_detail(pid: int, db: Session = Depends(get_db)):
    p = db.get(Product, pid)
    if not p or not p.active:
        raise HTTPException(404, "That product is not available.")
    # "bought together": what other customers put in the same basket, then fill up from the same department
    together = db.execute(
        select(OrderItem.product_id, func.count().label("n")).where(
            OrderItem.order_id.in_(select(OrderItem.order_id).where(OrderItem.product_id == pid).limit(500)),
            OrderItem.product_id != pid).group_by(OrderItem.product_id).order_by(func.count().desc()).limit(8)).all()
    picked = [p2 for p2 in (db.get(Product, i) for i, _ in together) if p2 and p2.active and p2.stock > 0][:4]
    if len(picked) < 4:
        have = {x.id for x in picked} | {p.id}
        more = db.scalars(select(Product).where(Product.active, Product.category_id == p.category_id, Product.id.notin_(have))
                          .order_by(func.random() if IS_POSTGRES else func.rand()).limit(4 - len(picked)))
        picked += list(more)
    return {**serialize.product(p), "related": [serialize.product(r) for r in picked], "bought_together": len(together) > 0}


# ------------------------------------------------------------------ checkout

@router.post("/checkout")
def checkout(body: schemas.Checkout, request: Request, db: Session = Depends(get_db),
             user: Optional[User] = Depends(security.optional_user)):
    if body.payment_method == "online" and not payments.enabled():
        raise HTTPException(400, "Online payment is not switched on in this shop. Choose another way to pay.")
    payments.expire_unpaid(db)
    order = place_order(db, user=user, name=body.customer_name.strip(), phone=body.phone, email=body.email.strip(),
                        address=body.address.strip(), zone_id=body.zone_id, payment_method=body.payment_method,
                        note=body.note.strip(), items=[i.model_dump() for i in body.items])
    db.commit()
    pay_url, pay_error = None, ""
    if order.payment_method == "online":
        try:
            pay_url = payments.start(db, order, str(request.base_url))
        except HTTPException as e:  # the order is saved; the customer can retry from the order page
            db.rollback()
            pay_error = e.detail
    return {"order": serialize.order(order), "pay_url": pay_url, "pay_error": pay_error}


def _digits(s: str) -> str:
    return "".join(c for c in s if c.isdigit())[-9:]


def _find(db: Session, number: str, phone: str, lock: bool = False) -> Order:
    """Number plus the phone it was ordered with. Same answer for 'no such order' and 'wrong phone'."""
    stmt = select(Order).where(Order.number == number.strip().upper())
    o = db.scalar(stmt.with_for_update() if lock else stmt)
    if not o or _digits(o.phone) != _digits(phone) or not _digits(phone):
        raise HTTPException(404, "We could not find an order with that number and phone.")
    return o


@router.post("/track")
def track(body: schemas.Track, db: Session = Depends(get_db)):
    return {"order": serialize.order(_find(db, body.number, body.phone))}


# ------------------------------------------------------------ online payment

@router.post("/orders/pay")
def pay_again(body: schemas.Track, request: Request, db: Session = Depends(get_db)):
    """Start (or restart) payment for an unpaid online order."""
    if not payments.enabled():
        raise HTTPException(400, "Online payment is not switched on in this shop.")
    o = _find(db, body.number, body.phone, lock=True)
    return {"pay_url": payments.start(db, o, str(request.base_url))}


@router.post("/orders/verify")
def verify_payment(body: schemas.Track, db: Session = Depends(get_db)):
    """The customer is back from the payment page. We ask the payment company what really happened."""
    o = _find(db, body.number, body.phone, lock=True)
    if o.payment_method == "online" and payments.enabled():
        payments.verify_order(db, o)
    return {"order": serialize.order(o)}


@router.post("/pay/webhook")
async def pay_webhook(request: Request, db: Session = Depends(get_db)):
    payments.webhook(db, await request.body(), request.headers.get("x-paystack-signature", ""))
    return {"ok": True}


class DemoPay(BaseModel):
    reference: str
    result: str


@router.get("/pay/demo/{reference}")
def demo_pay_info(reference: str, db: Session = Depends(get_db)):
    if config.PAYMENT_PROVIDER != "demo":
        raise HTTPException(404, "Not found.")
    p = db.scalar(select(Payment).where(Payment.reference == reference))
    if not p:
        raise HTTPException(404, "Payment not found.")
    o = db.get(Order, p.order_id)
    return {"order": o.number, "amount": float(p.amount), "status": p.status}


@router.post("/pay/demo/complete")
def demo_pay_complete(body: DemoPay, db: Session = Depends(get_db)):
    """The pretend payment page. Exists only in the demo shop; a real store answers 404."""
    if config.PAYMENT_PROVIDER != "demo" or body.result not in ("success", "failed"):
        raise HTTPException(404, "Not found.")
    p = db.scalar(select(Payment).where(Payment.reference == body.reference).with_for_update())
    if not p or p.status != "initiated":
        raise HTTPException(404, "Payment not found.")
    p.demo_result = body.result
    db.commit()
    return {"ok": True}


# ------------------------------------------------------------------ accounts

@router.post("/auth/register")
def register(body: schemas.Register, response: Response, db: Session = Depends(get_db)):
    problem = security.password_problem(body.password)
    if problem:
        raise HTTPException(400, problem)
    phone = schemas.clean_phone(body.phone) if body.phone.strip() else ""
    if db.scalar(select(User.id).where(User.email == body.email)):
        raise HTTPException(409, "There is already an account with that email. Try signing in.")
    u = User(name=body.name.strip(), email=body.email, phone=phone, password_hash=security.hash_password(body.password))
    db.add(u)
    db.commit()
    security.start_session(response, u)
    return {"user": serialize.user(u)}


@router.post("/auth/login")
def login(body: schemas.Login, request: Request, response: Response, db: Session = Depends(get_db)):
    email = body.email.strip().lower()
    key = email
    security.throttle_check(key)
    u = db.scalar(select(User).where(User.email == email))
    ok = security.check_password(body.password, u.password_hash) if u else security.check_password(body.password, _DUMMY)
    if not (u and ok and u.active):
        security.throttle_fail(key)
        raise HTTPException(401, "That email or password is not right.")
    security.throttle_clear(key)
    u.last_login = now()
    if u.role in ("owner", "staff"):
        audit(db, u.email, "sign_in")
    db.commit()
    security.start_session(response, u)
    return {"user": serialize.user(u)}


_DUMMY = security.hash_password("not-a-real-password-0")  # keeps timing equal for unknown emails


@router.post("/auth/logout")
def logout(response: Response):
    security.end_session(response)
    return {"ok": True}


@router.get("/auth/me")
def me(user: Optional[User] = Depends(security.optional_user)):
    return {"user": serialize.user(user) if user else None}


@router.patch("/account")
def update_profile(body: schemas.ProfileUpdate, db: Session = Depends(get_db), user: User = Depends(security.current_user)):
    user.name = body.name.strip()
    user.phone = schemas.clean_phone(body.phone) if body.phone.strip() else ""
    user.address = body.address.strip()
    db.commit()
    return {"user": serialize.user(user)}


@router.post("/account/password")
def change_password(body: schemas.PasswordChange, db: Session = Depends(get_db), user: User = Depends(security.current_user)):
    if not security.check_password(body.current, user.password_hash):
        raise HTTPException(400, "Your current password is not right.")
    problem = security.password_problem(body.new)
    if problem:
        raise HTTPException(400, problem)
    user.password_hash = security.hash_password(body.new)
    if user.role != "customer":
        audit(db, user.email, "change_password")
    db.commit()
    return {"ok": True}


@router.get("/account/orders")
def my_orders(db: Session = Depends(get_db), user: User = Depends(security.current_user)):
    rows = db.scalars(select(Order).where(Order.user_id == user.id).order_by(Order.id.desc()).limit(100))
    return [serialize.order(o) for o in rows]
