"""
Registration / OTP / GST Validation flow for RGE REGALGOA ERP AI.

Adds a multi-stage public-signup pipeline ON TOP of the existing
`/api/auth/register` endpoint (which remains untouched for backward
compatibility — used by legacy/admin-seeded clients).

New flow:
    POST /api/auth/register/gst-validate    → Live GSTIN format + checksum
    POST /api/auth/register/init            → Stage-1: validate + send OTP
    POST /api/auth/register/verify-otp      → Stage-2: confirm OTP code
    POST /api/auth/register/resend-otp      → Re-send OTP (cooldown enforced)
    POST /api/auth/register/complete        → Stage-3: create user + company

State is carried between stages by a short-lived signed JWT (`registration_token`)
that embeds the entered details so the client never has to repeat them.

OTP delivery: tries Meta WhatsApp Cloud API first (when configured),
gracefully falls back to email — and exposes the code in dev/stub mode
under EXPOSE_REG_OTP_CODE=1.

GSTIN validation: full structural check (state code 01-37, PAN format,
default 'Z' at position 14) PLUS the official base-36 checksum algorithm.
"""
from __future__ import annotations

import logging
import os
import re
import secrets
import string
from datetime import datetime, timedelta, timezone
from typing import Optional

import jwt
from bson import ObjectId
from fastapi import APIRouter, HTTPException, Request, Response
from pydantic import BaseModel, EmailStr

from auth import (
    JWT_ALGORITHM, create_access_token, create_refresh_token,
    get_jwt_secret, hash_password, set_auth_cookies, verify_password,
)
from whatsapp_auth import _meta_send_otp, _mask_phone, _normalize_phone, is_meta_configured

logger = logging.getLogger("rge.registration")

# ----------------------------------------------------------------------------
# Config
# ----------------------------------------------------------------------------
REG_OTP_TTL_SEC = 600                       # registration_token lifetime: 10 min
OTP_LEN = 6
OTP_VALIDITY_SEC = 600                      # actual OTP code TTL within the token: 10 min
OTP_RESEND_COOLDOWN_SEC = 30
OTP_MAX_TRIES = 5
EXPOSE_REG_OTP_CODE = os.environ.get("EXPOSE_REG_OTP_CODE", "1") == "1"


# ----------------------------------------------------------------------------
# GSTIN — structure + base-36 checksum (verified against GSTN spec)
# ----------------------------------------------------------------------------
GST_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
INDIAN_STATE_CODES = {
    "01": "Jammu and Kashmir", "02": "Himachal Pradesh", "03": "Punjab",
    "04": "Chandigarh", "05": "Uttarakhand", "06": "Haryana",
    "07": "Delhi", "08": "Rajasthan", "09": "Uttar Pradesh",
    "10": "Bihar", "11": "Sikkim", "12": "Arunachal Pradesh",
    "13": "Nagaland", "14": "Manipur", "15": "Mizoram",
    "16": "Tripura", "17": "Meghalaya", "18": "Assam",
    "19": "West Bengal", "20": "Jharkhand", "21": "Odisha",
    "22": "Chhattisgarh", "23": "Madhya Pradesh", "24": "Gujarat",
    "25": "Daman and Diu", "26": "Dadra and Nagar Haveli", "27": "Maharashtra",
    "28": "Andhra Pradesh (Before)", "29": "Karnataka", "30": "Goa",
    "31": "Lakshadweep", "32": "Kerala", "33": "Tamil Nadu",
    "34": "Puducherry", "35": "Andaman and Nicobar Islands", "36": "Telangana",
    "37": "Andhra Pradesh (New)", "38": "Ladakh",
    "97": "Other Territory", "99": "Centre Jurisdiction",
}


def gstin_checksum(g14: str) -> str:
    """Compute the 15th GSTIN check digit from the first 14 characters."""
    factor = 2
    s = 0
    for ch in g14[::-1]:
        d = GST_CHARS.index(ch)
        d *= factor
        d = (d // 36) + (d % 36)
        s += d
        factor = 3 - factor
    return GST_CHARS[(36 - (s % 36)) % 36]


def validate_gstin(gstin: str) -> dict:
    """Returns {'valid': bool, 'reason': str | None, 'state_code': str, 'state_name': str, 'pan': str}."""
    g = (gstin or "").strip().upper()
    if not g:
        return {"valid": False, "reason": "GSTIN is empty"}
    if len(g) != 15:
        return {"valid": False, "reason": "GSTIN must be exactly 15 characters"}
    if not all(c in GST_CHARS for c in g):
        return {"valid": False, "reason": "GSTIN must be alphanumeric (A-Z, 0-9)"}
    state_code = g[:2]
    state_name = INDIAN_STATE_CODES.get(state_code)
    if not state_name:
        return {"valid": False, "reason": f"Unknown state code '{state_code}'"}
    pan = g[2:12]
    if not (pan[:5].isalpha() and pan[5:9].isdigit() and pan[9].isalpha()):
        return {"valid": False, "reason": "Embedded PAN format is invalid (expected AAAAA9999A)"}
    entity = g[12]
    if not (entity.isalnum()):
        return {"valid": False, "reason": "Entity code (position 13) must be alphanumeric"}
    if g[13] != "Z":
        return {"valid": False, "reason": "Position 14 should be 'Z' (default check character)"}
    expected_checksum = gstin_checksum(g[:14])
    if g[14] != expected_checksum:
        return {"valid": False, "reason": "Checksum digit (position 15) does not match"}
    return {
        "valid": True, "reason": None,
        "state_code": state_code, "state_name": state_name, "pan": pan,
        "entity_code": entity,
    }


# ----------------------------------------------------------------------------
# Registration token (JWT carrying staged signup data)
# ----------------------------------------------------------------------------
def _issue_reg_token(payload: dict, otp_verified: bool = False) -> str:
    body = dict(payload)
    body.update({
        "type": "registration",
        "otp_verified": bool(otp_verified),
        "exp": datetime.now(timezone.utc) + timedelta(seconds=REG_OTP_TTL_SEC),
        "iat": datetime.now(timezone.utc),
    })
    return jwt.encode(body, get_jwt_secret(), algorithm=JWT_ALGORITHM)


def _decode_reg_token(token: str) -> dict:
    try:
        decoded = jwt.decode(token, get_jwt_secret(), algorithms=[JWT_ALGORITHM])
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "Registration session expired — please start again")
    except jwt.InvalidTokenError:
        raise HTTPException(401, "Invalid registration session")
    if decoded.get("type") != "registration":
        raise HTTPException(401, "Wrong token type")
    return decoded


# ----------------------------------------------------------------------------
# Email helper (best-effort, never blocks). Uses existing email_service if avail.
# ----------------------------------------------------------------------------
async def _send_email_otp(to: str, code: str) -> bool:
    """Send OTP via email. Returns True on success."""
    try:
        from email_service import send_email_async  # type: ignore
        subject = f"Your RGE Regalgoa ERP AI verification code: {code}"
        body = (
            f"Welcome to RGE REGALGOA ERP AI!\n\n"
            f"Your verification code is: {code}\n\n"
            f"This code expires in 10 minutes. Do not share it with anyone.\n\n"
            f"If you didn't request this, simply ignore this email."
        )
        await send_email_async(to, subject, body)
        return True
    except Exception as ex:
        logger.warning("Email OTP send failed: %s", ex)
        return False


def _gen_otp(n: int = OTP_LEN) -> str:
    return "".join(secrets.choice(string.digits) for _ in range(n))


def _valid_email(email: str) -> bool:
    return bool(re.match(r"^[^\s@]+@[^\s@]+\.[^\s@]+$", (email or "").strip()))


def _valid_phone(phone: str) -> bool:
    n = re.sub(r"\D", "", phone or "")
    return 8 <= len(n) <= 15


# ----------------------------------------------------------------------------
# Router
# ----------------------------------------------------------------------------
router = APIRouter(prefix="/api/auth/register", tags=["registration"])


class GstValidateIn(BaseModel):
    gstin: str


@router.post("/gst-validate")
async def gst_validate(payload: GstValidateIn):
    """Public — validates GSTIN format + checksum + state code (no GSTN API call).
    Used by the registration form for instant inline feedback as the user types.
    """
    result = validate_gstin(payload.gstin)
    return result


class RegisterInitIn(BaseModel):
    first_name: str
    last_name: str
    email: EmailStr
    phone: str
    password: str
    # OTP delivery channel — "whatsapp" | "email" | "auto"
    channel: Optional[str] = "auto"


@router.post("/init")
async def register_init(payload: RegisterInitIn, request: Request):
    """Stage 1 — validate basic account inputs, generate OTP, send it,
    return a short-lived `registration_token` carrying the payload to stage 2.
    """
    db = request.app.state.db
    fn = (payload.first_name or "").strip()
    ln = (payload.last_name or "").strip()
    email = (payload.email or "").strip().lower()
    phone = (payload.phone or "").strip()
    pw = payload.password or ""

    # ---- input validation ----
    if not fn or not ln:
        raise HTTPException(422, "First and last name are required")
    if not _valid_email(email):
        raise HTTPException(422, "Invalid email address")
    if not _valid_phone(phone):
        raise HTTPException(422, "Phone number is invalid")
    if len(pw) < 8:
        raise HTTPException(422, "Password must be at least 8 characters")
    if pw.lower() in ("password", "12345678", "qwerty12", "admin123"):
        raise HTTPException(422, "Password is too common — please choose a stronger one")

    # Duplicate email?
    if await db.users.find_one({"email": email}):
        raise HTTPException(409, "An account with this email already exists. Try logging in instead.")

    # Generate OTP and pick delivery channel.
    code = _gen_otp()
    norm_phone = _normalize_phone(phone)
    delivery = "stub"
    channel = (payload.channel or "auto").lower()

    # Prefer WhatsApp when (a) provider configured AND (b) user didn't force email.
    used_channels = []
    if channel in ("whatsapp", "auto"):
        try:
            await _meta_send_otp(norm_phone, code)
            delivery = "whatsapp" if is_meta_configured() else "stub"
            used_channels.append("whatsapp" if is_meta_configured() else "stub-whatsapp")
        except HTTPException as ex:
            logger.warning("Registration WA send failed: %s", ex.detail)
        except Exception as ex:
            logger.exception("Registration WA send unexpected error: %s", ex)

    # Email fallback (best-effort; we don't fail registration if email fails)
    if channel in ("email", "auto") and delivery in ("stub", "failed"):
        ok = await _send_email_otp(email, code)
        if ok:
            used_channels.append("email")
            if delivery == "stub" and not is_meta_configured():
                # Still mark as stub since user can see code in dev mode
                pass
            elif delivery == "failed":
                delivery = "email"

    if delivery == "stub" and not is_meta_configured() and "email" not in used_channels:
        # No real channel succeeded — but stub mode means user sees code on screen
        used_channels.append("stub")

    # Store hash + counter in token so we don't need a DB write
    code_hash = hash_password(code)
    token = _issue_reg_token({
        "fn": fn, "ln": ln, "email": email, "phone": norm_phone,
        "ph": hash_password(pw),
        "otp_hash": code_hash,
        "otp_expires_at": (datetime.now(timezone.utc) + timedelta(seconds=OTP_VALIDITY_SEC)).isoformat(),
        "tries": 0,
        "issued_at": datetime.now(timezone.utc).isoformat(),
        "delivered_via": delivery,
        "channels": used_channels,
    }, otp_verified=False)

    # Audit
    await db.audit_log.insert_one({
        "ts": datetime.now(timezone.utc).isoformat(),
        "action": "auth.register.init", "outcome": "ok",
        "email": email, "ip": request.client.host if request.client else "",
        "user_agent": (request.headers.get("user-agent") or "")[:240],
        "detail": {"delivered_via": delivery, "channels": used_channels},
    })

    resp = {
        "ok": True,
        "registration_token": token,
        "masked_phone": _mask_phone(norm_phone),
        "masked_email": _mask_email(email),
        "delivered_via": delivery,
        "channels": used_channels,
        "expires_in": OTP_VALIDITY_SEC,
        "resend_after": OTP_RESEND_COOLDOWN_SEC,
    }
    if EXPOSE_REG_OTP_CODE and not is_meta_configured():
        resp["code_preview"] = code
    return resp


def _mask_email(email: str) -> str:
    if "@" not in email:
        return email
    local, dom = email.split("@", 1)
    if len(local) <= 2:
        return local[0] + "*@" + dom
    return local[0] + "***" + local[-1] + "@" + dom


class VerifyOtpIn(BaseModel):
    registration_token: str
    code: str


@router.post("/verify-otp")
async def register_verify_otp(payload: VerifyOtpIn, request: Request):
    """Stage 2 — verify the OTP code; returns an UPDATED registration_token
    marked `otp_verified: true`. The frontend then calls /complete with
    the final business details + this verified token."""
    decoded = _decode_reg_token(payload.registration_token)
    if decoded.get("otp_verified"):
        # Already verified — idempotent re-issue
        return {"ok": True, "registration_token": _issue_reg_token(decoded, otp_verified=True)}
    # Expiry on the OTP itself (independent of token TTL)
    try:
        exp = datetime.fromisoformat(decoded["otp_expires_at"])
        if exp.tzinfo is None:
            exp = exp.replace(tzinfo=timezone.utc)
    except (KeyError, ValueError, TypeError):
        raise HTTPException(401, "Corrupted session")
    if exp < datetime.now(timezone.utc):
        raise HTTPException(401, "OTP expired — please request a new code")

    tries = int(decoded.get("tries", 0))
    if tries >= OTP_MAX_TRIES:
        raise HTTPException(429, "Too many attempts — please request a new OTP")

    code_in = (payload.code or "").strip()
    if not (code_in.isdigit() and len(code_in) == OTP_LEN):
        raise HTTPException(400, f"OTP must be {OTP_LEN} digits")

    if not verify_password(code_in, decoded["otp_hash"]):
        # Increment tries — re-issue the same token with bumped count
        decoded["tries"] = tries + 1
        _issue_reg_token(decoded, otp_verified=False)  # tries-bumped token would go here if we DB-persisted
        await request.app.state.db.audit_log.insert_one({
            "ts": datetime.now(timezone.utc).isoformat(),
            "action": "auth.register.otp_failed", "outcome": "denied",
            "email": decoded.get("email"),
            "detail": {"tries": tries + 1},
        })
        raise HTTPException(401, "Wrong OTP code")

    # Verified — issue verified token (kept short to limit window)
    decoded["tries"] = 0
    verified_token = _issue_reg_token(decoded, otp_verified=True)
    await request.app.state.db.audit_log.insert_one({
        "ts": datetime.now(timezone.utc).isoformat(),
        "action": "auth.register.otp_verified", "outcome": "ok",
        "email": decoded.get("email"),
    })
    return {"ok": True, "registration_token": verified_token}


class ResendOtpIn(BaseModel):
    registration_token: str


@router.post("/resend-otp")
async def register_resend_otp(payload: ResendOtpIn, request: Request):
    """Resend — issues a fresh OTP & token. Old token becomes invalid because
    a new otp_hash overrides the previous."""
    decoded = _decode_reg_token(payload.registration_token)
    if decoded.get("otp_verified"):
        raise HTTPException(409, "OTP already verified — proceed to complete registration")

    # Cooldown check
    issued = datetime.fromisoformat(decoded.get("issued_at"))
    if issued.tzinfo is None:
        issued = issued.replace(tzinfo=timezone.utc)
    elapsed = (datetime.now(timezone.utc) - issued).total_seconds()
    if elapsed < OTP_RESEND_COOLDOWN_SEC:
        wait = int(OTP_RESEND_COOLDOWN_SEC - elapsed)
        raise HTTPException(429, f"Please wait {wait}s before requesting another OTP")

    code = _gen_otp()
    norm_phone = decoded.get("phone")
    delivery = "stub"
    used_channels = []
    try:
        await _meta_send_otp(norm_phone, code)
        delivery = "whatsapp" if is_meta_configured() else "stub"
        used_channels.append(delivery)
    except Exception as ex:
        logger.warning("Resend WA failed: %s", ex)
    if delivery in ("stub", "failed"):
        if await _send_email_otp(decoded.get("email"), code):
            used_channels.append("email")
            if delivery == "failed":
                delivery = "email"

    decoded["otp_hash"] = hash_password(code)
    decoded["otp_expires_at"] = (datetime.now(timezone.utc) + timedelta(seconds=OTP_VALIDITY_SEC)).isoformat()
    decoded["tries"] = 0
    decoded["issued_at"] = datetime.now(timezone.utc).isoformat()
    decoded["delivered_via"] = delivery
    decoded["channels"] = used_channels
    new_token = _issue_reg_token(decoded, otp_verified=False)

    resp = {
        "ok": True,
        "registration_token": new_token,
        "masked_phone": _mask_phone(norm_phone),
        "masked_email": _mask_email(decoded.get("email", "")),
        "delivered_via": delivery,
        "channels": used_channels,
        "expires_in": OTP_VALIDITY_SEC,
        "resend_after": OTP_RESEND_COOLDOWN_SEC,
    }
    if EXPOSE_REG_OTP_CODE and not is_meta_configured():
        resp["code_preview"] = code
    return resp


class CompleteIn(BaseModel):
    registration_token: str
    # MANDATORY company creation:
    company_name: str
    gstin: Optional[str] = None
    state: Optional[str] = None
    address: Optional[str] = None
    business_type: Optional[str] = None


@router.post("/complete")
async def register_complete(payload: CompleteIn, request: Request, response: Response):
    """Stage 3 — atomically create the user, the company, and auto-login.
    Requires an OTP-verified registration_token."""
    decoded = _decode_reg_token(payload.registration_token)
    if not decoded.get("otp_verified"):
        raise HTTPException(403, "Please verify the OTP first")

    db = request.app.state.db
    email = (decoded.get("email") or "").lower().strip()
    if not email:
        raise HTTPException(401, "Corrupted session — please start again")

    # Race-safe duplicate check
    if await db.users.find_one({"email": email}):
        raise HTTPException(409, "Account already exists — please log in")

    company_name = (payload.company_name or "").strip()
    if not company_name:
        raise HTTPException(422, "Company name is required")

    # GSTIN validation (if provided, must be fully valid — checksum + structure)
    gstin = (payload.gstin or "").strip().upper()
    gst_meta = {}
    if gstin:
        v = validate_gstin(gstin)
        if not v["valid"]:
            raise HTTPException(422, f"GSTIN invalid: {v['reason']}")
        gst_meta = v
        # Override state from GSTIN if user didn't pass one
        if not (payload.state or "").strip():
            payload.state = v["state_name"]

    # First-user-becomes-admin policy
    user_count = await db.users.count_documents({})
    assigned_role = "admin" if user_count == 0 else "staff"

    now_iso = datetime.now(timezone.utc).isoformat()
    ip = request.client.host if request.client else "unknown"
    ua = (request.headers.get("user-agent") or "")[:240]

    user_doc = {
        "email": email,
        "password_hash": decoded.get("ph"),  # bcrypt hash carried from /init
        "name": f"{decoded.get('fn', '')} {decoded.get('ln', '')}".strip(),
        "first_name": decoded.get("fn", ""),
        "last_name": decoded.get("ln", ""),
        "company_name": company_name,
        "phone": decoded.get("phone", ""),
        "gstin": gstin,
        "address": (payload.address or "").strip(),
        "state": (payload.state or "").strip(),
        "business_type": (payload.business_type or "").strip(),
        "role": assigned_role,
        "is_active": True,
        "twofa_enabled": False,
        "wa_otp_enabled": False,
        "created_at": now_iso,
        "updated_at": now_iso,
        "signup_ip": ip,
        "signup_user_agent": ua,
        "signup_channel": "registration_wizard_v1",
        "plan": "free",
        "license_status": "trial",
        "email_verified": True,   # we just OTP'd — treat phone/email as verified
        "phone_verified": True,
    }
    res = await db.users.insert_one(user_doc)
    uid = str(res.inserted_id)

    # Company creation is MANDATORY at this stage — if it fails, we roll back
    # the user insert. This honours the spec ("Mandatory Company Creation").
    try:
        await db.companies.insert_one({
            "name": company_name,
            "owner_id": uid,
            "gstin": gstin,
            "address": (payload.address or "").strip(),
            "state": (payload.state or "").strip() or gst_meta.get("state_name", ""),
            "state_code": gst_meta.get("state_code", ""),
            "pan": gst_meta.get("pan", ""),
            "phone": decoded.get("phone", ""),
            "email": email,
            "industry": (payload.business_type or "").strip() or "General",
            "created_at": now_iso,
        })
    except Exception as ex:
        # Rollback the user — cannot leave an orphan account behind
        await db.users.delete_one({"_id": res.inserted_id})
        logger.exception("Company creation failed, rolled back user")
        raise HTTPException(500, f"Could not finalize registration: {ex}")

    # Audit + auto-login
    await db.audit_log.insert_one({
        "ts": now_iso,
        "user_id": uid, "email": email,
        "action": "auth.register.complete", "outcome": "ok",
        "ip": ip, "user_agent": ua,
        "detail": {"role": assigned_role, "company": company_name, "gstin_provided": bool(gstin)},
    })
    access = create_access_token(uid, email, assigned_role)
    refresh = create_refresh_token(uid)
    set_auth_cookies(response, access, refresh)

    return {
        "id": uid,
        "email": email,
        "name": user_doc["name"],
        "role": assigned_role,
        "company": company_name,
        "gstin": gstin,
    }
