from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import check_password as django_check_password
from django.conf import settings
from django.db import DatabaseError, connections
import logging
from rest_framework.authtoken.models import Token
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response


logger = logging.getLogger(__name__)


class LegacyDbUnavailable(RuntimeError):
    pass


def _normalize_bcrypt_hash(hash_str: str) -> str:
    # Laravel often stores bcrypt hashes with $2y$ prefix.
    # Python bcrypt expects $2a$/$2b$; $2y$ is compatible with $2b$.
    if hash_str.startswith("$2y$"):
        return "$2b$" + hash_str[4:]
    return hash_str


def _check_bcrypt_password(plain_or_hash: str, stored_hash: str) -> bool:
    # Accept if user mistakenly pastes the stored hash in UI (common during testing)
    if plain_or_hash == stored_hash:
        return True

    if stored_hash.startswith("$2"):
        try:
            import bcrypt  # type: ignore
        except Exception:
            logger.warning("auth.token bcrypt import failed")
            return False

        try:
            normalized = _normalize_bcrypt_hash(stored_hash)
            return bcrypt.checkpw(plain_or_hash.encode("utf-8"), normalized.encode("utf-8"))
        except Exception:
            logger.warning("auth.token bcrypt check failed", exc_info=True)
            return False

    return django_check_password(plain_or_hash, stored_hash)


_ADMIN_ROLES = frozenset({"admin", "superadmin", "super_admin", "owner"})


def _normalize_role_string(role_val) -> str:
    if role_val is None:
        return ""
    return str(role_val).strip().lower().replace(" ", "_")


def _coerce_effective_role(bia_role_raw: str | None, legacy: dict, ui_hint: str) -> str:
    """
    Prefer cluster `business_id_agents.role`, then legacy `users.role`, then login UI hint.
    """
    if bia_role_raw is not None and str(bia_role_raw).strip() != "":
        return _normalize_role_string(bia_role_raw) or "agent"
    lr = legacy.get("role")
    if lr is not None and str(lr).strip() != "":
        return _normalize_role_string(lr) or "agent"
    if ui_hint:
        return _normalize_role_string(ui_hint) or "agent"
    return "agent"


def _django_is_privileged(role_norm: str) -> bool:
    return role_norm in _ADMIN_ROLES


def _fetch_role_from_business_id_agents(business_id: int, legacy: dict, identifier: str) -> str | None:
    """
    Read dashboard role from cluster `business_id_agents.role` when that column (and
    identifying columns) exist. Schema may vary; unsupported columns are skipped.
    """
    email_norm = str(legacy.get("email") or "").strip().lower()
    ident_norm = str(identifier or "").strip().lower()
    agent_id = legacy.get("agent_id")
    user_id = legacy.get("user_id")

    attempts: list[tuple[str, list]] = []
    if agent_id is not None and str(agent_id).strip() != "":
        attempts.append(
            (
                "SELECT role FROM business_id_agents WHERE business_id = %s AND agent_id = %s LIMIT 1",
                [business_id, agent_id],
            )
        )
    if user_id is not None and str(user_id).strip() != "":
        attempts.append(
            (
                "SELECT role FROM business_id_agents WHERE business_id = %s AND user_id = %s LIMIT 1",
                [business_id, user_id],
            )
        )
    if email_norm or ident_norm:
        attempts.append(
            (
                """
                SELECT role FROM business_id_agents
                WHERE business_id = %s
                  AND LOWER(TRIM(COALESCE(email, ''))) IN (%s, %s)
                LIMIT 1
                """,
                [business_id, email_norm or ident_norm, ident_norm or email_norm],
            )
        )
        attempts.append(
            (
                """
                SELECT role FROM business_id_agents
                WHERE business_id = %s
                  AND LOWER(TRIM(COALESCE(username, ''))) IN (%s, %s)
                LIMIT 1
                """,
                [business_id, email_norm or ident_norm, ident_norm or email_norm],
            )
        )

    try:
        with connections["cluster"].cursor() as cur:
            for sql, params in attempts:
                try:
                    cur.execute(sql, params)
                    row = cur.fetchone()
                    if row and row[0] is not None and str(row[0]).strip() != "":
                        return str(row[0]).strip()
                except DatabaseError:
                    continue
    except DatabaseError:
        return None
    return None


def _fetch_legacy_user(identifier: str):
    """
    Query the legacy master DB `users` table.
    Expected columns (common Laravel-style): user_id, username, email, password, role, business_id, agent_id, status.
    """
    sql = """
        SELECT user_id, username, email, password, role, business_id, agent_id, status, name
        FROM users
        WHERE (email = %s OR username = %s)
        LIMIT 1
    """
    try:
        with connections["default"].cursor() as cur:
            cur.execute(sql, [identifier, identifier])
            row = cur.fetchone()
            if not row:
                return None

            colnames = [c[0] for c in cur.description]
            data = dict(zip(colnames, row))
    except DatabaseError as e:
        # MySQL unreachable / credentials wrong / network blocked.
        logger.error("legacy master DB unavailable while fetching user", exc_info=True)
        raise LegacyDbUnavailable(str(e)) from e

    # Optional status check
    status_val = (data.get("status") or "").lower()
    if status_val and status_val not in ("active", "1", "enabled"):
        # if status is set and not active-ish, block login
        return None

    return data


def _cluster_acknowledges_business(
    business_id: int, legacy: dict, identifier: str, effective_role: str | None = None
) -> bool:
    """
    Cluster may store agents either in Django's mapping table `business_id_agents`
    or in per-business tables `{business_id}_agents` (legacy / Laravel-style).

    Agents logging in via the dashboard should appear in the per-business agents
    table with the same email when that table is used.
    """
    from apps.cluster.dynamic_tables import _table_names, _q_ident

    role_lower = _normalize_role_string(effective_role or legacy.get("role") or "")
    email_norm = str(legacy.get("email") or "").strip().lower()
    ident_norm = str(identifier or "").strip().lower()

    try:
        with connections["cluster"].cursor() as cur:
            cur.execute(
                "SELECT 1 FROM business_id_agents WHERE business_id = %s LIMIT 1",
                [business_id],
            )
            if cur.fetchone():
                return True
    except DatabaseError:
        pass

    agents_table, _, _, _ = _table_names(business_id)
    try:
        with connections["cluster"].cursor() as cur:
            cur.execute(
                f"SELECT 1 FROM {_q_ident(agents_table)} WHERE business_id = %s LIMIT 1",
                [business_id],
            )
            if not cur.fetchone():
                return False
            if role_lower in _ADMIN_ROLES:
                return True
            cur.execute(
                f"""
                SELECT 1 FROM {_q_ident(agents_table)}
                WHERE business_id = %s
                  AND (
                    LOWER(email) = %s
                    OR LOWER(email) = %s
                  )
                LIMIT 1
                """,
                [business_id, email_norm, ident_norm],
            )
            return cur.fetchone() is not None
    except DatabaseError:
        return False


def _auth_error(message: str, status: int = 400):
    return Response(
        {"non_field_errors": [message], "message": message},
        status=status,
    )


@api_view(["POST"])
@permission_classes([AllowAny])
def legacy_token(request):
    """
    Custom token endpoint compatible with your React UI + legacy MySQL tables.
    Body: { "username": "...", "password": "...", "role": "admin|agent" }
    Returns: { "token": "...", "user": {...} }
    """
    identifier = (request.data.get("username") or request.data.get("email") or "").strip()
    password = request.data.get("password") or ""
    role = (request.data.get("role") or "").strip()  # optional

    logger.warning(
        "auth.token attempt identifier=%s role=%s has_password=%s",
        identifier,
        role,
        bool(password),
    )

    if not identifier or not password:
        return _auth_error("Missing username or password.")

    try:
        legacy = _fetch_legacy_user(identifier)
    except LegacyDbUnavailable as e:
        # Include a safe hint (host/port/db) so operators can see what the running
        # backend is actually trying to connect to (common issue: container still
        # points to public DNS instead of host-gateway/private IP).
        try:
            dflt = (settings.DATABASES.get("default") or {}).copy()
            clus = (settings.DATABASES.get("cluster") or {}).copy()
            dflt_hint = f"{dflt.get('HOST')}:{dflt.get('PORT')} / {dflt.get('NAME')}"
            clus_hint = f"{clus.get('HOST')}:{clus.get('PORT')} / {clus.get('NAME')}"
        except Exception:
            dflt_hint = "<unknown>"
            clus_hint = "<unknown>"
        # Don’t 500: return a clear message for local/dev setups where the legacy DB isn’t reachable.
        return _auth_error(
            "Legacy database is not reachable from this backend runtime. "
            "Check network/firewall and ensure the running process loaded the correct DB URLs. "
            f"(default={dflt_hint}; cluster={clus_hint}) "
            f"(error={type(e).__name__})",
            status=503,
        )
    if not legacy:
        logger.warning("auth.token legacy user not found/blocked for identifier=%s", identifier)
        return _auth_error("Unable to log in with provided credentials.")

    stored_hash = (legacy.get("password") or "").strip()
    logger.warning(
        "auth.token password_check identifier=%s password_len=%s stored_prefix=%s",
        identifier,
        len(password or ""),
        stored_hash[:12],
    )
    if not stored_hash or not _check_bcrypt_password(password, stored_hash):
        logger.warning(
            "auth.token password mismatch identifier=%s user_id=%s role=%s",
            identifier,
            legacy.get("user_id"),
            legacy.get("role"),
        )
        return _auth_error("Unable to log in with provided credentials.")

    business_id = legacy.get("business_id")
    if business_id is None:
        logger.warning("auth.token missing business_id identifier=%s user_id=%s", identifier, legacy.get("user_id"))
        return _auth_error("User is missing business_id.")

    try:
        business_id_int = int(business_id)
    except Exception:
        return _auth_error("Invalid business_id.")

    bia_role_raw = _fetch_role_from_business_id_agents(business_id_int, legacy, identifier)
    role_lower = _coerce_effective_role(bia_role_raw, legacy, role)

    if not _cluster_acknowledges_business(
        business_id_int, legacy, identifier, effective_role=role_lower
    ):
        logger.warning(
            "auth.token cluster check failed identifier=%s user_id=%s business_id=%s",
            identifier,
            legacy.get("user_id"),
            business_id_int,
        )
        return _auth_error(
            "No matching agent configuration in the cluster database for this user or business."
        )

    # Create/Get a Django auth user so DRF TokenAuthentication works.
    DjangoUser = get_user_model()
    django_username = str(legacy.get("username") or legacy.get("email") or identifier)[:150]
    django_user, created = DjangoUser.objects.get_or_create(username=django_username)
    if created:
        django_user.set_unusable_password()

    django_user.email = str(legacy.get("email") or "")

    privileged = _django_is_privileged(role_lower)
    django_user.is_staff = privileged
    django_user.is_superuser = privileged
    django_user.save(update_fields=["email", "is_staff", "is_superuser"] + ([] if not created else ["password"]))

    token, _ = Token.objects.get_or_create(user=django_user)

    return Response(
        {
            "token": token.key,
            "user": {
                "id": str(legacy.get("user_id") or django_user.id),
                "name": str(legacy.get("name") or legacy.get("username") or ""),
                "email": str(legacy.get("email") or ""),
                "role": role_lower,
                "agentId": legacy.get("agent_id"),
                "business_id": business_id_int,
                "permissions": [],
            },
        }
    )


@api_view(["GET"])
@permission_classes([IsAuthenticated])
def me(request):
    u = request.user
    dj_role = "admin" if (getattr(u, "is_staff", False) or getattr(u, "is_superuser", False)) else "agent"
    ident = (getattr(u, "email", None) or getattr(u, "username", None) or "").strip()
    legacy = _fetch_legacy_user(ident) if ident else None
    business_id = None
    agent_id = None
    legacy_user_id = None
    role_out = dj_role
    if legacy:
        business_id = legacy.get("business_id")
        agent_id = legacy.get("agent_id")
        legacy_user_id = legacy.get("user_id")
        if business_id is not None:
            try:
                business_id = int(business_id)
            except Exception:
                business_id = None
        bia = (
            _fetch_role_from_business_id_agents(business_id, legacy, ident)
            if business_id is not None
            else None
        )
        role_out = _coerce_effective_role(bia, legacy, "")
    return Response(
        {
            "id": str(legacy_user_id or getattr(u, "id", "")),
            "username": getattr(u, "username", ""),
            "email": getattr(u, "email", ""),
            "name": (getattr(u, "get_full_name", lambda: "")() or getattr(u, "username", "")),
            "role": role_out,
            "agentId": agent_id,
            "business_id": business_id,
            "permissions": [],
        }
    )


@api_view(["GET"])
@permission_classes([IsAuthenticated])
def business_plan(request):
    """
    Return current user's business record fields needed by the dashboard.
    Source of truth: legacy master DB table `businesses`, column `plans`.
    """
    u = request.user
    ident = (getattr(u, "email", None) or getattr(u, "username", None) or "").strip()
    legacy = _fetch_legacy_user(ident) if ident else None
    business_id = legacy.get("business_id") if legacy else None
    try:
        business_id_int = int(business_id) if business_id is not None else None
    except Exception:
        business_id_int = None

    if not business_id_int:
        return Response({"message": "Business not found for current user."}, status=404)

    # Read from DB to ensure we return the current `plans` value.
    with connections["default"].cursor() as cur:
        try:
            # Optional ch_language column (may or may not exist depending on schema).
            cur.execute(
                "SELECT business_id, business_name, plans, ch_language FROM businesses WHERE business_id = %s LIMIT 1",
                [business_id_int],
            )
            row = cur.fetchone()
            if not row:
                return Response({"message": "Business not found."}, status=404)
            return Response(
                {
                    "business_id": row[0],
                    "business_name": row[1],
                    "plans": row[2],
                    "ch_language": row[3],
                }
            )
        except DatabaseError:
            cur.execute(
                "SELECT business_id, business_name, plans FROM businesses WHERE business_id = %s LIMIT 1",
                [business_id_int],
            )
            row = cur.fetchone()
            if not row:
                return Response({"message": "Business not found."}, status=404)
            return Response(
                {
                    "business_id": row[0],
                    "business_name": row[1],
                    "plans": row[2],
                    "ch_language": None,
                }
            )

