"""Company security settings: IP whitelist and login OTP."""
import hashlib
import ipaddress
import logging
import re
import secrets
import uuid
from datetime import timedelta
from typing import Optional

from django.utils import timezone

from human_resource.models import StaffProfile
from system_administration.email_settings import dispatch_general_mail
from system_administration.models import CompanyProfile, CompanySecuritySettings, LoginOtpChallenge

logger = logging.getLogger(__name__)


def get_or_create_security_settings(company_profile: CompanyProfile) -> CompanySecuritySettings:
    settings_obj, _created = CompanySecuritySettings.objects.get_or_create(
        company_profile=company_profile,
    )
    return settings_obj


def security_settings_to_map(settings_obj: CompanySecuritySettings) -> dict:
    entries = parse_allowed_ips(settings_obj.allowed_ips or '')
    return {
        'enable_ip_whitelist': settings_obj.enable_ip_whitelist,
        'allowed_ips': settings_obj.allowed_ips or '',
        'allowed_ip_count': len(entries),
        'enable_login_otp': settings_obj.enable_login_otp,
        'otp_length': settings_obj.otp_length,
        'otp_ttl_minutes': settings_obj.otp_ttl_minutes,
        'otp_max_attempts': settings_obj.otp_max_attempts,
    }


def resolve_company_from_staff(staff_profile: StaffProfile) -> Optional[CompanyProfile]:
    branch = staff_profile.company_branch
    if branch is not None and branch.company_profile is not None:
        return branch.company_profile
    return None


def client_ip_from_request(request) -> str:
    forwarded = request.META.get('HTTP_X_FORWARDED_FOR')
    if forwarded:
        return forwarded.split(',')[0].strip()
    return (request.META.get('REMOTE_ADDR') or '').strip()


def parse_allowed_ips(raw: str) -> list[str]:
    if not raw or not str(raw).strip():
        return []
    parts = re.split(r'[\n,;]+', str(raw))
    return [part.strip() for part in parts if part.strip()]


def _ip_matches_entry(client_ip: str, entry: str) -> bool:
    try:
        if '/' in entry:
            network = ipaddress.ip_network(entry, strict=False)
            return ipaddress.ip_address(client_ip) in network
        return ipaddress.ip_address(client_ip) == ipaddress.ip_address(entry)
    except ValueError:
        return False


def is_system_administrator(staff_profile: StaffProfile) -> bool:
    pos = staff_profile.staff_position
    return bool(
        pos is not None
        and pos.position_title == 'System Administrator'
        and staff_profile.is_head_of_department is True
        and staff_profile.has_read_write_priviledges is True
    )


def is_ip_allowed_for_staff(
    request,
    staff_profile: StaffProfile,
    settings_obj: CompanySecuritySettings,
) -> bool:
    if not settings_obj.enable_ip_whitelist:
        return True
    if is_system_administrator(staff_profile):
        return True

    client_ip = client_ip_from_request(request)
    if not client_ip:
        return False

    entries = parse_allowed_ips(settings_obj.allowed_ips)
    if not entries:
        return False

    return any(_ip_matches_entry(client_ip, entry) for entry in entries)


def mask_email_address(email: str) -> str:
    email = (email or '').strip()
    if '@' not in email:
        return 'your email'
    local, domain = email.split('@', 1)
    if len(local) <= 1:
        masked_local = '*'
    else:
        masked_local = f'{local[0]}{"*" * min(3, len(local) - 1)}'
    return f'{masked_local}@{domain}'


def _hash_otp(challenge_id: str, otp_code: str) -> str:
    return hashlib.sha256(f'{challenge_id}:{otp_code}'.encode()).hexdigest()


def _generate_otp(length: int) -> str:
    length = max(4, min(int(length or 6), 8))
    return ''.join(str(secrets.randbelow(10)) for _ in range(length))


def create_login_otp_challenge(
    staff_profile: StaffProfile,
    settings_obj: CompanySecuritySettings,
    company_profile: CompanyProfile,
) -> dict:
    email = (staff_profile.email_address or '').strip()
    if not email:
        return {'sent': False, 'reason': 'missing_email'}

    challenge_id = uuid.uuid4().hex
    otp_code = _generate_otp(settings_obj.otp_length)
    expires_at = timezone.now() + timedelta(minutes=max(1, int(settings_obj.otp_ttl_minutes or 10)))

    LoginOtpChallenge.objects.filter(
        staff_profile=staff_profile,
        expires_at__lt=timezone.now(),
    ).delete()

    LoginOtpChallenge.objects.create(
        challenge_id=challenge_id,
        staff_profile=staff_profile,
        otp_hash=_hash_otp(challenge_id, otp_code),
        expires_at=expires_at,
    )

    subject = 'Megawatt ERP — login verification code'
    message = (
        f'Your login verification code is: {otp_code}\n\n'
        f'This code expires in {settings_obj.otp_ttl_minutes} minutes.\n'
        'If you did not attempt to sign in, contact your system administrator.'
    )
    sent = dispatch_general_mail(
        subject,
        message,
        [email],
        company_profile=company_profile,
    )
    if not sent:
        LoginOtpChallenge.objects.filter(challenge_id=challenge_id).delete()
        return {'sent': False, 'reason': 'email_failed'}

    return {
        'sent': True,
        'challenge_id': challenge_id,
        'masked_email': mask_email_address(email),
    }


def verify_login_otp_challenge(staff_number: str, challenge_id: str, otp_code: str) -> tuple[bool, str, Optional[StaffProfile]]:
    staff_number = (staff_number or '').strip()
    challenge_id = (challenge_id or '').strip()
    otp_code = (otp_code or '').strip()

    if not staff_number or not challenge_id or not otp_code:
        return False, 'Verification code is required.', None

    try:
        staff_profile = StaffProfile.objects.select_related(
            'user', 'company_branch', 'company_department', 'staff_position',
        ).get(staff_number=staff_number)
    except StaffProfile.DoesNotExist:
        return False, 'Invalid verification request.', None

    try:
        challenge = LoginOtpChallenge.objects.get(challenge_id=challenge_id, staff_profile=staff_profile)
    except LoginOtpChallenge.DoesNotExist:
        return False, 'Verification session expired. Sign in again.', None

    if challenge.expires_at < timezone.now():
        challenge.delete()
        return False, 'Verification code expired. Sign in again.', None

    company = resolve_company_from_staff(staff_profile)
    max_attempts = 5
    if company is not None:
        security = get_or_create_security_settings(company)
        max_attempts = max(1, int(security.otp_max_attempts or 5))

    if challenge.attempts >= max_attempts:
        challenge.delete()
        return False, 'Too many failed attempts. Sign in again.', None

    if _hash_otp(challenge_id, otp_code) != challenge.otp_hash:
        challenge.attempts += 1
        challenge.save(update_fields=['attempts'])
        remaining = max_attempts - challenge.attempts
        if remaining <= 0:
            challenge.delete()
            return False, 'Too many failed attempts. Sign in again.', None
        return False, f'Invalid verification code. {remaining} attempt(s) left.', None

    challenge.delete()
    return True, 'Verified', staff_profile


def build_staff_login_payload(staff_profile: StaffProfile, user, token) -> dict:
    username = user.username
    pos = staff_profile.staff_position
    full_name = (
        f'{staff_profile.first_name} {staff_profile.last_name}'
        if staff_profile.first_name and staff_profile.last_name
        else 'No Name'
    )
    user_role = pos.position_title if pos is not None else 'Not Assigned'

    # Dynamic RBAC: ship the user's effective permissions + CASL rules so the
    # SPA gates modules/actions from real roles. Imported lazily to avoid a
    # hard dependency during early migrations / circular imports.
    try:
        from access_control.abilities import build_ability_rules, effective_permissions
        ability_rules = build_ability_rules(staff_profile)
        permissions = effective_permissions(staff_profile)
    except Exception:
        ability_rules = []
        permissions = []

    return {
        'user_token': token.key,
        'permissions': permissions,
        'ability_rules': ability_rules,
        'staff_id': str(staff_profile.id),
        'first_name': staff_profile.first_name or '',
        'last_name': staff_profile.last_name or '',
        'email_address': staff_profile.email_address or '',
        'username': username,
        'branch_name': staff_profile.company_branch.branch_name if staff_profile.company_branch is not None else '',
        'phone_number': staff_profile.phone_number or '',
        'is_super_admin': bool(
            pos is not None
            and pos.position_title == 'System Administrator'
            and staff_profile.is_head_of_department is True
            and staff_profile.has_read_write_priviledges is True
        ),
        'company_department': (
            staff_profile.company_department.department_name
            if staff_profile.company_department is not None
            else 'not_assigned'
        ),
        'full_name': full_name,
        'user_role': user_role,
        'otp_required': False,
    }
