import os
from datetime import datetime

from flask_login import UserMixin
from werkzeug.security import check_password_hash, generate_password_hash

from app import db
from app.utils.secret_crypto import decrypt_if_needed, encrypt_if_possible, is_configured as secrets_encryption_configured


class User(UserMixin, db.Model):
    """User model for username-based authentication"""

    __tablename__ = "users"
    __table_args__ = (db.UniqueConstraint("oidc_issuer", "oidc_sub", name="uq_users_oidc_issuer_sub"),)

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False, index=True)
    email = db.Column(db.String(200), nullable=True, index=True)
    full_name = db.Column(db.String(200), nullable=True)
    role = db.Column(db.String(20), default="user", nullable=False)  # 'user' or 'admin'
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
    last_login = db.Column(db.DateTime, nullable=True)
    is_active = db.Column(db.Boolean, default=True, nullable=False)
    theme_preference = db.Column(db.String(10), default=None, nullable=True)  # 'light' | 'dark' | None=system
    preferred_language = db.Column(db.String(8), default=None, nullable=True)  # e.g., 'en', 'de'
    # Admin update popup: normalized semver of last "don't show again" GitHub release
    dismissed_release_version = db.Column(db.String(64), nullable=True)
    oidc_sub = db.Column(db.String(255), nullable=True)
    oidc_issuer = db.Column(db.String(255), nullable=True)
    # Authentication source: local password, OIDC, or LDAP (synced on login where applicable)
    auth_provider = db.Column(db.String(20), nullable=False, default="local", server_default="local")
    avatar_filename = db.Column(db.String(255), nullable=True)
    password_hash = db.Column(db.String(255), nullable=True)
    password_change_required = db.Column(
        db.Boolean, default=False, nullable=False
    )  # Force password change on first login

    # Two-factor authentication (TOTP)
    two_factor_enabled = db.Column(db.Boolean, default=False, nullable=False)
    two_factor_secret = db.Column(db.String(255), nullable=True)  # encrypted-at-rest when key configured
    two_factor_confirmed_at = db.Column(db.DateTime, nullable=True)

    # User preferences and settings
    email_notifications = db.Column(db.Boolean, default=True, nullable=False)  # Enable/disable email notifications
    notification_overdue_invoices = db.Column(db.Boolean, default=True, nullable=False)  # Notify about overdue invoices
    notification_task_assigned = db.Column(db.Boolean, default=True, nullable=False)  # Notify when assigned to task
    notification_task_comments = db.Column(db.Boolean, default=True, nullable=False)  # Notify about task comments
    notification_weekly_summary = db.Column(db.Boolean, default=False, nullable=False)  # Send weekly time summary
    notification_remind_to_log = db.Column(
        db.Boolean, default=False, nullable=False
    )  # Remind to log time at end of day
    reminder_to_log_time = db.Column(
        db.String(5), nullable=True
    )  # Time of day "HH:MM" (24h) for reminder, e.g. "17:00"
    # In-app smart notifications (separate from email remind-to-log)
    smart_notifications_enabled = db.Column(db.Boolean, default=False, nullable=False)
    smart_notify_no_tracking = db.Column(db.Boolean, default=True, nullable=False)
    smart_notify_long_timer = db.Column(db.Boolean, default=True, nullable=False)
    smart_notify_daily_summary = db.Column(db.Boolean, default=True, nullable=False)
    smart_notify_browser = db.Column(db.Boolean, default=False, nullable=False)
    smart_notify_no_tracking_after = db.Column(db.String(5), nullable=True)  # HH:MM override; null = use app config
    smart_notify_summary_at = db.Column(db.String(5), nullable=True)  # HH:MM override; null = use app config
    timezone = db.Column(db.String(50), nullable=True)  # User-specific timezone override
    date_format = db.Column(db.String(20), default=None, nullable=True)  # None = use system default
    time_format = db.Column(db.String(10), default=None, nullable=True)  # None = use system default
    week_start_day = db.Column(db.Integer, default=1, nullable=False)  # 0=Sunday, 1=Monday, etc.

    # Time rounding preferences
    time_rounding_enabled = db.Column(db.Boolean, default=True, nullable=False)  # Enable/disable time rounding
    time_rounding_minutes = db.Column(db.Integer, default=1, nullable=False)  # Rounding interval: 1, 5, 10, 15, 30, 60
    time_rounding_method = db.Column(db.String(10), default="nearest", nullable=False)  # 'nearest', 'up', or 'down'

    # Overtime settings
    standard_hours_per_day = db.Column(
        db.Float, default=8.0, nullable=False
    )  # Standard working hours per day for overtime calculation
    overtime_include_weekends = db.Column(
        db.Boolean, default=True, nullable=False
    )  # If True, weekend hours count toward regular/overtime; if False, all weekend hours count as overtime
    overtime_calculation_mode = db.Column(
        db.String(10), default="daily", nullable=False
    )  # 'daily' | 'weekly': overtime by daily cap vs weekly cap
    standard_hours_per_week = db.Column(db.Float, nullable=True)  # Used when overtime_calculation_mode is 'weekly'

    # Client portal settings
    client_portal_enabled = db.Column(db.Boolean, default=False, nullable=False)  # Enable/disable client portal access
    client_id = db.Column(
        db.Integer, db.ForeignKey("clients.id", ondelete="SET NULL"), nullable=True, index=True
    )  # Link user to a client for portal access

    # Calendar item type colors (hex e.g. #3b82f6); when null, app uses defaults
    calendar_color_events = db.Column(db.String(7), nullable=True)
    calendar_color_tasks = db.Column(db.String(7), nullable=True)
    calendar_color_time_entries = db.Column(db.String(7), nullable=True)
    # Calendar default view: 'day' | 'week' | 'month'; None = use last view (session)
    calendar_default_view = db.Column(db.String(10), nullable=True)

    # Keyboard shortcut overrides: JSON dict { "shortcut_id": "normalized_key" }. None/empty = use defaults.
    keyboard_shortcuts_overrides = db.Column(db.JSON, nullable=True)

    # UI feature flags - allow users to customize which features are visible
    # All default to True (enabled) for backward compatibility
    # Calendar section
    ui_show_calendar = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Calendar section

    # Time Tracking section items
    ui_show_project_templates = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Project Templates
    ui_show_gantt_chart = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Gantt Chart
    ui_show_kanban_board = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Kanban Board
    ui_show_weekly_goals = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Weekly Goals
    ui_show_issues = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Issues feature

    # CRM section
    ui_show_quotes = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Quotes

    # Finance & Expenses section items
    ui_show_reports = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Reports
    ui_show_report_builder = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Report Builder
    ui_show_scheduled_reports = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Scheduled Reports
    ui_show_invoice_approvals = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Invoice Approvals
    ui_show_payment_gateways = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Payment Gateways
    ui_show_recurring_invoices = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Recurring Invoices
    ui_show_payments = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Payments
    ui_show_mileage = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Mileage
    ui_show_per_diem = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Per Diem
    ui_show_budget_alerts = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Budget Alerts

    # Inventory section
    ui_show_inventory = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Inventory section

    # Analytics
    ui_show_analytics = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Analytics

    # Tools & Data section
    ui_show_tools = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Tools & Data section
    ui_show_integrations = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Integrations
    ui_show_import_export = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Import/Export
    ui_show_saved_filters = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Saved Filters

    # CRM section (additional)
    ui_show_contacts = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Contacts
    ui_show_deals = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Deals
    ui_show_leads = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Leads

    # Finance section (additional)
    ui_show_invoices = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Invoices
    ui_show_expenses = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Expenses

    # Time Tracking section (additional)
    ui_show_time_entry_templates = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Time Entry Templates

    # Advanced features
    ui_show_workflows = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Workflows
    ui_show_time_approvals = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Time Approvals
    ui_show_activity_feed = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Activity Feed
    ui_show_recurring_tasks = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Recurring Tasks
    ui_show_team_chat = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Team Chat
    ui_show_client_portal = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Client Portal
    ui_show_kiosk = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide Kiosk Mode
    ui_show_donate = db.Column(db.Boolean, default=True, nullable=False)  # Show/hide donate/support UI

    # Support UX: count of report generations (exports + custom report views) for stats in support modal
    support_stats_reports_generated = db.Column(db.Integer, default=0, nullable=False)

    # Relationships
    time_entries = db.relationship("TimeEntry", backref="user", lazy="dynamic", cascade="all, delete-orphan")
    project_costs = db.relationship("ProjectCost", backref="user", lazy="dynamic", cascade="all, delete-orphan")
    favorite_projects = db.relationship(
        "Project",
        secondary="user_favorite_projects",
        lazy="dynamic",
        backref=db.backref("favorited_by", lazy="dynamic"),
    )
    roles = db.relationship("Role", secondary="user_roles", lazy="joined", backref=db.backref("users", lazy="dynamic"))
    client = db.relationship("Client", backref="portal_users", lazy="joined")
    assigned_clients = db.relationship(
        "Client",
        secondary="user_clients",
        lazy="dynamic",
        backref=db.backref("assigned_users", lazy="dynamic"),
    )

    def __init__(self, username, role="user", email=None, full_name=None):
        self.username = username.lower().strip()
        self.role = role
        self.email = email or None
        self.full_name = full_name or None
        # Set default for standard_hours_per_day if not set by SQLAlchemy
        if not hasattr(self, "standard_hours_per_day") or self.standard_hours_per_day is None:
            self.standard_hours_per_day = 8.0

    def __repr__(self):
        return f"<User {self.username}>"

    def set_password(self, password):
        """
        Set the user's password hash.
        For OIDC users, password is optional.
        """
        if password:
            self.password_hash = generate_password_hash(password)
        else:
            self.password_hash = None

    def check_password(self, password):
        """
        Check if the provided password matches the user's password hash.
        Returns False if no password is set or if password doesn't match.
        """
        if not self.password_hash or not password:
            return False
        return check_password_hash(self.password_hash, password)

    @property
    def has_password(self):
        """Check if user has a password set"""
        return bool(self.password_hash)

    def set_two_factor_secret(self, secret: str) -> None:
        secret = (secret or "").strip()
        if not secret:
            self.two_factor_secret = None
            return
        if secrets_encryption_configured():
            self.two_factor_secret = encrypt_if_possible(secret)
        else:
            self.two_factor_secret = secret

    def get_two_factor_secret(self) -> str:
        return decrypt_if_needed(self.two_factor_secret or "")

    @property
    def is_admin(self):
        """Check if user is an admin"""
        # Backward compatibility: check legacy role field first
        if self.role == "admin":
            return True
        # Check if user has any admin role
        return any(role.name in ["admin", "super_admin"] for role in self.roles)

    @property
    def is_super_admin(self):
        """Check if user is a super admin"""
        # Check if user has super_admin role
        return any(role.name == "super_admin" for role in self.roles)

    @property
    def active_timer(self):
        """Get the user's currently active timer"""
        from .time_entry import TimeEntry

        return TimeEntry.query.filter_by(user_id=self.id, end_time=None).first()

    @property
    def total_hours(self):
        """Calculate total hours worked by this user"""
        from .time_entry import TimeEntry

        total_seconds = (
            db.session.query(db.func.sum(TimeEntry.duration_seconds))
            .filter(TimeEntry.user_id == self.id, TimeEntry.end_time.isnot(None))
            .scalar()
            or 0
        )
        return round(total_seconds / 3600, 2)

    @property
    def display_name(self):
        """Preferred display name: full name if available, else username"""
        if self.full_name and self.full_name.strip():
            return self.full_name.strip()
        return self.username

    def get_recent_entries(self, limit=10):
        """Get recent time entries for this user"""
        from .time_entry import TimeEntry

        return (
            self.time_entries.filter(TimeEntry.end_time.isnot(None))
            .order_by(TimeEntry.start_time.desc())
            .limit(limit)
            .all()
        )

    def update_last_login(self):
        """Update the last login timestamp"""
        self.last_login = datetime.utcnow()
        db.session.commit()

    def is_online(self):
        """Check if user is currently online (active within last 15 minutes)"""
        if not self.last_login:
            return False
        from datetime import timedelta

        threshold = datetime.utcnow() - timedelta(minutes=15)
        return self.last_login >= threshold

    def get_status(self):
        """Get user status: 'online', 'offline', or 'away'"""
        if not self.last_login:
            return "offline"

        from datetime import timedelta

        now = datetime.utcnow()
        time_since_login = now - self.last_login

        # Online if active within last 15 minutes
        if time_since_login <= timedelta(minutes=15):
            return "online"
        # Away if active within last 1 hour
        elif time_since_login <= timedelta(hours=1):
            return "away"
        # Offline otherwise
        else:
            return "offline"

    def to_dict(self, total_hours_override=None):
        """Convert user to dictionary for API responses.
        Includes resolved date_format and time_format (user override or system default) for clients (e.g. mobile).
        total_hours_override: optional precomputed total hours (avoids N+1 when serializing many users).
        """
        from app.utils.timezone import (
            get_app_timezone,
            get_resolved_date_format_key,
            get_resolved_time_format_key,
            get_user_timezone_name,
        )

        try:
            resolved_date = get_resolved_date_format_key(self)
            resolved_time = get_resolved_time_format_key(self)
            resolved_timezone = get_user_timezone_name(self) or get_app_timezone()
        except Exception:
            resolved_date = "YYYY-MM-DD"
            resolved_time = "24h"
            resolved_timezone = "Europe/Rome"
        total_hours = total_hours_override if total_hours_override is not None else self.total_hours
        return {
            "id": self.id,
            "username": self.username,
            "email": self.email,
            "full_name": self.full_name,
            "display_name": self.display_name,
            "role": self.role,
            "created_at": self.created_at.isoformat() if self.created_at else None,
            "last_login": self.last_login.isoformat() if self.last_login else None,
            "is_active": self.is_active,
            "total_hours": total_hours,
            "avatar_url": self.get_avatar_url(),
            "status": self.get_status(),
            "date_format": resolved_date,
            "time_format": resolved_time,
            "timezone": resolved_timezone,
            "standard_hours_per_day": float(getattr(self, "standard_hours_per_day", 8.0) or 8.0),
            "overtime_include_weekends": getattr(self, "overtime_include_weekends", True),
        }

    # Avatar helpers
    def get_avatar_url(self):
        """Return the public URL for the user's avatar, or None if not set"""
        if self.avatar_filename:
            return f"/uploads/avatars/{self.avatar_filename}"
        return None

    def get_avatar_path(self):
        """Return absolute filesystem path to the user's avatar, or None if not set"""
        if not self.avatar_filename:
            return None
        try:
            from flask import current_app

            # Avatars are now stored in /data volume to persist between container updates
            upload_folder = os.path.join(current_app.config.get("UPLOAD_FOLDER", "/data/uploads"), "avatars")
            return os.path.join(upload_folder, self.avatar_filename)
        except Exception:
            # Fallback for development/non-docker environments
            return os.path.join("/data/uploads", "avatars", self.avatar_filename)

    def has_avatar(self):
        """Check whether the user's avatar file exists on disk"""
        path = self.get_avatar_path()
        return bool(path and os.path.exists(path))

    # Favorite projects helpers
    def add_favorite_project(self, project):
        """Add a project to user's favorites"""
        if not self.is_project_favorite(project):
            self.favorite_projects.append(project)
            db.session.commit()

    def remove_favorite_project(self, project):
        """Remove a project from user's favorites"""
        if self.is_project_favorite(project):
            self.favorite_projects.remove(project)
            db.session.commit()

    def is_project_favorite(self, project):
        """Check if a project is in user's favorites"""
        from .project import Project

        if isinstance(project, int):
            project_id = project
            return self.favorite_projects.filter_by(id=project_id).count() > 0
        elif isinstance(project, Project):
            return self.favorite_projects.filter_by(id=project.id).count() > 0
        return False

    def get_favorite_projects(self, status="active"):
        """Get user's favorite projects, optionally filtered by status"""
        query = self.favorite_projects
        if status:
            query = query.filter_by(status=status)
        return query.order_by("name").all()

    # Permission and role helpers
    def has_permission(self, permission_name):
        """Check if user has a specific permission through any of their roles"""
        # Auto-assign role from legacy role field if user has no roles assigned
        if not self.roles and self.role:
            self._auto_assign_role_from_legacy()

        # Super admin users have all permissions
        if self.role == "admin" and not self.roles:
            # Legacy admin users without roles have all permissions
            return True

        # Check if any of the user's roles have this permission
        for role in self.roles:
            if role.has_permission(permission_name):
                return True

        # Fallback: Check legacy role field if no roles assigned
        # This handles cases where role assignment failed or user is in transition
        if not self.roles and self.role:
            from app.models import Role

            legacy_role = Role.query.filter_by(name=self.role).first()
            if legacy_role and legacy_role.has_permission(permission_name):
                return True

        return False

    def _auto_assign_role_from_legacy(self):
        """Auto-assign role from legacy role field if user has no roles assigned"""
        if self.roles or not self.role:
            return

        from app.models import Role

        role_obj = Role.query.filter_by(name=self.role).first()
        if role_obj:
            self.roles.append(role_obj)
            try:
                db.session.commit()
            except Exception:
                db.session.rollback()

    def has_any_permission(self, *permission_names):
        """Check if user has any of the specified permissions"""
        return any(self.has_permission(perm) for perm in permission_names)

    def has_all_permissions(self, *permission_names):
        """Check if user has all of the specified permissions"""
        return all(self.has_permission(perm) for perm in permission_names)

    def add_role(self, role):
        """Add a role to this user"""
        if role not in self.roles:
            self.roles.append(role)

    def remove_role(self, role):
        """Remove a role from this user"""
        if role in self.roles:
            self.roles.remove(role)

    def get_all_permissions(self):
        """Get all permissions this user has through their roles"""
        permissions = set()
        for role in self.roles:
            for permission in role.permissions:
                permissions.add(permission)
        return list(permissions)

    def get_role_names(self):
        """Get list of role names for this user"""
        return [r.name for r in self.roles]

    @property
    def primary_role_name(self):
        """Get the primary role name for display purposes.
        Returns the first role name from roles, or falls back to the legacy role field."""
        if self.roles:
            # Return the first role name (roles are typically ordered by importance)
            return self.roles[0].name
        # Fallback to legacy role field for backward compatibility
        return self.role

    # Subcontractor / scope restriction (assigned clients only)
    @property
    def is_scope_restricted(self):
        """True if user is restricted to assigned clients (e.g. subcontractor role)."""
        return "subcontractor" in self.get_role_names()

    @property
    def is_client_portal_user(self):
        """Check if user has client portal access enabled"""
        return self.client_portal_enabled and self.client_id is not None

    def get_allowed_client_ids(self):
        """Return list of client IDs this user may access, or None for full access."""
        if self.is_admin:
            return None
        if self.is_client_portal_user:
            return [self.client_id] if self.client_id else []
        if not self.is_scope_restricted:
            return None
        ids = [c.id for c in self.assigned_clients.all()]
        return ids if ids else []

    def get_allowed_project_ids(self):
        """Return list of project IDs this user may access, or None for full access."""
        if self.is_admin:
            return None
        from .project import Project

        client_ids = self.get_allowed_client_ids()
        if client_ids is None:
            return None
        if not client_ids:
            return []
        rows = db.session.query(Project.id).filter(Project.client_id.in_(client_ids)).all()
        return [r[0] for r in rows]

    # Client portal helpers
    def get_client_portal_data(self):
        """Get data for client portal view (projects, invoices, time entries for assigned client)"""
        if not self.is_client_portal_user:
            return None

        from .client import Client
        from .invoice import Invoice
        from .project import Project
        from .time_entry import TimeEntry

        # Get client - try relationship first, then query by ID if needed
        client = self.client
        if not client and self.client_id:
            # Relationship might not be loaded, query directly
            client = Client.query.get(self.client_id)

        if not client:
            return None

        # Get active projects for this client
        projects = Project.query.filter_by(client_id=client.id, status="active").order_by(Project.name).all()

        # Get invoices for this client
        invoices = Invoice.query.filter_by(client_id=client.id).order_by(Invoice.issue_date.desc()).limit(50).all()

        # Get time entries for projects belonging to this client
        project_ids = [p.id for p in projects]
        time_entries = (
            TimeEntry.query.filter(TimeEntry.project_id.in_(project_ids), TimeEntry.end_time.isnot(None))
            .order_by(TimeEntry.start_time.desc())
            .limit(100)
            .all()
        )

        return {"client": client, "projects": projects, "invoices": invoices, "time_entries": time_entries}
