"""RGE REGALGOA — GRN (Goods Received Note) module.

A GRN documents the physical receipt of goods from a supplier against a
purchase bill / order. Typical fields:

  * Auto-generated GRN number (RGE/GRN/{YY-YY}/{n})
  * Supplier (party) info, snapshot at creation time
  * Vehicle no., transporter, gate-pass / LR no., received-by name
  * Line items with `qty_ordered`, `qty_received` and `qty_short`
  * Remarks per line + run-level notes / attachments
  * Status: draft → received → cancelled

Why a dedicated collection (and not just an attribute on purchases)?
  - A single purchase bill can be received in multiple shipments → many GRNs.
  - Stock movement happens on GRN, not at the time the bill is recorded
    (Indian SMEs receive goods first, bill comes later).
  - Allows reverse-print + audit trail without touching the purchase bill.

Endpoints (all prefixed with /api/grns):
  GET    /              – list GRNs for active company
  POST   /              – create a GRN (optionally from a purchase bill)
  GET    /{id}          – single GRN with lines
  PUT    /{id}          – patch a draft GRN (no edits once cancelled)
  POST   /{id}/cancel   – mark GRN as cancelled (audit trail kept)
  DELETE /{id}          – soft-delete to grns_trash
  GET    /from-purchase/{purchase_id} – seed GRN payload from a purchase bill
"""
from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import List, Optional

from bson import ObjectId
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel, Field

from auth import get_current_user, require_admin
from permissions import require_permission

logger = logging.getLogger("rm-regal.grn")

router = APIRouter(prefix="/api/grns", tags=["grns"])


# ----------------------------------------------------------------------------- 
# Helpers
# ----------------------------------------------------------------------------- 
def _oid(v: str) -> ObjectId:
    try:
        return ObjectId(v)
    except Exception:
        raise HTTPException(400, "Invalid id")


def _ser(doc: dict) -> dict:
    if not doc:
        return doc
    out = dict(doc)
    out["id"] = str(out.pop("_id"))
    return out


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _fy_label() -> str:
    """Indian FY label e.g. 25-26 (Apr→Mar)."""
    now = datetime.now(timezone.utc)
    y = now.year
    if now.month < 4:
        return f"{(y - 1) % 100:02d}-{y % 100:02d}"
    return f"{y % 100:02d}-{(y + 1) % 100:02d}"


async def _next_grn_no(db, company_id: str) -> str:
    fy = _fy_label()
    prefix = f"GRN/{fy}/"
    # Look up last number issued for this company in current FY.
    last = await db.grns.find(
        {"company_id": company_id, "grn_no": {"$regex": f"^{prefix}"}}
    ).sort("grn_no", -1).limit(1).to_list(1)
    n = 1
    if last:
        try:
            n = int(str(last[0]["grn_no"]).split("/")[-1]) + 1
        except Exception:
            n = 1
    return f"{prefix}{n}"


# ----------------------------------------------------------------------------- 
# Models
# ----------------------------------------------------------------------------- 
class GrnLine(BaseModel):
    item_id: Optional[str] = None
    name: str
    hsn: Optional[str] = ""
    unit: Optional[str] = "PCS"
    qty_ordered: float = 0.0
    qty_received: float = 0.0
    rate: float = 0.0
    remarks: Optional[str] = ""


class GrnIn(BaseModel):
    purchase_id: Optional[str] = None         # link to source purchase bill (optional)
    purchase_no: Optional[str] = ""
    party_id: Optional[str] = None
    party_name: Optional[str] = ""
    party_gstin: Optional[str] = ""
    grn_date: Optional[str] = None
    vehicle_no: Optional[str] = ""
    transport_name: Optional[str] = ""
    lr_no: Optional[str] = ""                 # LR / docket / e-way bill no.
    gate_pass_no: Optional[str] = ""
    received_by: Optional[str] = ""
    delivery_location: Optional[str] = ""
    notes: Optional[str] = ""
    lines: List[GrnLine] = Field(default_factory=list)
    status: Optional[str] = "received"        # draft | received | cancelled


class GrnPatch(BaseModel):
    grn_date: Optional[str] = None
    vehicle_no: Optional[str] = None
    transport_name: Optional[str] = None
    lr_no: Optional[str] = None
    gate_pass_no: Optional[str] = None
    received_by: Optional[str] = None
    delivery_location: Optional[str] = None
    notes: Optional[str] = None
    lines: Optional[List[GrnLine]] = None
    status: Optional[str] = None


# ----------------------------------------------------------------------------- 
# CRUD
# ----------------------------------------------------------------------------- 
@router.get("")
async def list_grns(
    request: Request,
    company_id: str = Query(...),
    party_id: Optional[str] = None,
    purchase_id: Optional[str] = None,
    status: Optional[str] = None,
    user=Depends(get_current_user),
):
    db = request.app.state.db
    q = {"company_id": company_id}
    if party_id:
        q["party_id"] = party_id
    if purchase_id:
        q["purchase_id"] = purchase_id
    if status:
        q["status"] = status
    docs = await db.grns.find(q).sort("grn_date", -1).to_list(2000)
    return [_ser(d) for d in docs]


@router.post("")
async def create_grn(payload: GrnIn, request: Request, company_id: str = Query(...), user=Depends(get_current_user)):
    db = request.app.state.db
    doc = payload.model_dump()
    doc["company_id"] = company_id
    doc["grn_no"] = await _next_grn_no(db, company_id)
    if not doc.get("grn_date"):
        doc["grn_date"] = datetime.now(timezone.utc).date().isoformat()
    doc["created_at"] = _now_iso()
    doc["created_by"] = user["email"]
    # Compute totals on lines for convenience
    for ln in doc.get("lines", []):
        ln["qty_short"] = round(max(0.0, float(ln.get("qty_ordered") or 0) - float(ln.get("qty_received") or 0)), 3)

    res = await db.grns.insert_one(doc)
    out = await db.grns.find_one({"_id": res.inserted_id})
    return _ser(out)


@router.get("/from-purchase/{purchase_id}")
async def seed_from_purchase(purchase_id: str, request: Request, user=Depends(get_current_user)):
    """Return a fully-populated GrnIn payload derived from a purchase bill — the
    frontend uses this to pre-fill the new-GRN form with one click."""
    db = request.app.state.db
    inv = await db.invoices.find_one({"_id": _oid(purchase_id), "type": "purchase"})
    if not inv:
        raise HTTPException(404, "Purchase bill not found")
    return {
        "purchase_id": purchase_id,
        "purchase_no": inv.get("invoice_no", ""),
        "party_id": inv.get("party_id"),
        "party_name": inv.get("party_name", ""),
        "party_gstin": inv.get("party_gstin", ""),
        "grn_date": datetime.now(timezone.utc).date().isoformat(),
        "vehicle_no": inv.get("vehicle_no", ""),
        "transport_name": inv.get("transport_name", ""),
        "delivery_location": inv.get("delivery_location", ""),
        "notes": "",
        "lines": [
            {
                "item_id": ln.get("item_id"),
                "name": ln.get("name", ""),
                "hsn": ln.get("hsn", ""),
                "unit": ln.get("unit", "PCS"),
                "qty_ordered": float(ln.get("qty") or 0),
                "qty_received": float(ln.get("qty") or 0),
                "rate": float(ln.get("rate") or 0),
                "remarks": "",
            }
            for ln in inv.get("lines", [])
        ],
        "status": "received",
    }


@router.get("/{grn_id}")
async def get_grn(grn_id: str, request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    doc = await db.grns.find_one({"_id": _oid(grn_id)})
    if not doc:
        raise HTTPException(404, "GRN not found")
    return _ser(doc)


@router.put("/{grn_id}")
async def update_grn(grn_id: str, payload: GrnPatch, request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    existing = await db.grns.find_one({"_id": _oid(grn_id)})
    if not existing:
        raise HTTPException(404, "GRN not found")
    if existing.get("status") == "cancelled":
        raise HTTPException(400, "Cancelled GRN cannot be edited")

    patch = {k: v for k, v in payload.model_dump(exclude_unset=True).items() if v is not None}
    if "lines" in patch:
        for ln in patch["lines"]:
            ln["qty_short"] = round(max(0.0, float(ln.get("qty_ordered") or 0) - float(ln.get("qty_received") or 0)), 3)
    patch["updated_at"] = _now_iso()
    patch["updated_by"] = user["email"]
    await db.grns.update_one({"_id": _oid(grn_id)}, {"$set": patch})
    out = await db.grns.find_one({"_id": _oid(grn_id)})
    return _ser(out)


@router.post("/{grn_id}/cancel")
async def cancel_grn(grn_id: str, request: Request, user=Depends(get_current_user)):
    db = request.app.state.db
    res = await db.grns.update_one(
        {"_id": _oid(grn_id), "status": {"$ne": "cancelled"}},
        {"$set": {"status": "cancelled", "cancelled_at": _now_iso(), "cancelled_by": user["email"]}},
    )
    if res.matched_count == 0:
        raise HTTPException(400, "GRN already cancelled or not found")
    return {"ok": True}


@router.delete("/{grn_id}")
async def delete_grn(grn_id: str, request: Request, user=Depends(require_permission("invoicing.delete"))):
    """Soft-delete GRN — moves the doc to grns_trash (mirrors the rest of the app)."""
    db = request.app.state.db
    doc = await db.grns.find_one({"_id": _oid(grn_id)})
    if not doc:
        raise HTTPException(404, "GRN not found")
    doc["deleted_at"] = _now_iso()
    doc["deleted_by"] = user["email"]
    await db.grns_trash.insert_one(doc)
    await db.grns.delete_one({"_id": _oid(grn_id)})
    return {"ok": True}
