import os
import threading
from datetime import datetime

from app import db
from app.config import Config
from app.utils.invoice_numbering import DEFAULT_INVOICE_PATTERN
from app.utils.secret_crypto import decrypt_if_needed, encrypt_if_possible, is_configured as secrets_encryption_configured

# Re-entrancy guard: avoid add+commit when get_settings is called from inside a flush/commit
_creating_settings = threading.local()


def _session_in_flush(session):
    """Return True if the session is currently in a flush (to avoid nested add+commit)."""
    try:
        # SQLAlchemy sets _flushing for the outer flush() call.
        if getattr(session, "_flushing", False):
            return True
        # During flush_context.execute(), SA 2.0 sets _warn_on_events while disallowing
        # Session.add() etc. ScopedSession/proxy edge cases can leave _flushing unreadable;
        # _warn_on_events reliably marks the "execution stage" of a flush.
        if getattr(session, "_warn_on_events", False):
            return True
        # Fallback: in a transaction and inside a flush context (if exposed)
        if (
            getattr(session, "in_transaction", lambda: False)()
            and getattr(session, "_current_flush_context", None) is not None
        ):
            return True
        return False
    except Exception:
        return False


class Settings(db.Model):
    """Settings model for system configuration"""

    __tablename__ = "settings"

    id = db.Column(db.Integer, primary_key=True)
    timezone = db.Column(db.String(50), default="Europe/Rome", nullable=False)
    date_format = db.Column(
        db.String(20), default="YYYY-MM-DD", nullable=False
    )  # YYYY-MM-DD, MM/DD/YYYY, DD/MM/YYYY, DD.MM.YYYY
    time_format = db.Column(db.String(10), default="24h", nullable=False)  # 24h or 12h
    currency = db.Column(db.String(3), default="EUR", nullable=False)
    rounding_minutes = db.Column(db.Integer, default=1, nullable=False)
    single_active_timer = db.Column(db.Boolean, default=True, nullable=False)
    allow_self_register = db.Column(db.Boolean, default=True, nullable=False)
    idle_timeout_minutes = db.Column(db.Integer, default=30, nullable=False)
    backup_retention_days = db.Column(db.Integer, default=30, nullable=False)
    backup_time = db.Column(db.String(5), default="02:00", nullable=False)  # HH:MM format
    export_delimiter = db.Column(db.String(1), default=",", nullable=False)

    # Company branding for invoices
    company_name = db.Column(db.String(200), default="Your Company Name", nullable=False)
    company_address = db.Column(db.Text, default="Your Company Address", nullable=False)
    company_email = db.Column(db.String(200), default="info@yourcompany.com", nullable=False)
    company_phone = db.Column(db.String(50), default="+1 (555) 123-4567", nullable=False)
    company_website = db.Column(db.String(200), default="www.yourcompany.com", nullable=False)
    company_logo_filename = db.Column(db.String(255), default="", nullable=True)  # Changed from company_logo_path
    company_tax_id = db.Column(db.String(100), default="", nullable=True)
    company_bank_info = db.Column(db.Text, default="", nullable=True)

    # PDF template customization
    invoice_pdf_template_html = db.Column(db.Text, default="", nullable=True)
    invoice_pdf_template_css = db.Column(db.Text, default="", nullable=True)
    invoice_pdf_design_json = db.Column(db.Text, default="", nullable=True)  # Konva.js design state

    # Invoice defaults
    invoice_prefix = db.Column(db.String(50), default="INV", nullable=False)
    invoice_number_pattern = db.Column(db.String(120), default=DEFAULT_INVOICE_PATTERN, nullable=False)
    invoice_start_number = db.Column(db.Integer, default=1000, nullable=False)
    invoice_terms = db.Column(db.Text, default="Payment is due within 30 days of invoice date.", nullable=False)
    invoice_notes = db.Column(db.Text, default="Thank you for your business!", nullable=False)

    # Peppol e-invoicing (optional; can be configured via WebUI or env)
    # peppol_enabled: None => use env var PEPPOL_ENABLED; True/False overrides env.
    peppol_enabled = db.Column(db.Boolean, default=None, nullable=True)
    peppol_sender_endpoint_id = db.Column(db.String(100), default="", nullable=True)
    peppol_sender_scheme_id = db.Column(db.String(20), default="", nullable=True)
    peppol_sender_country = db.Column(db.String(2), default="", nullable=True)
    peppol_access_point_url = db.Column(db.String(500), default="", nullable=True)
    peppol_access_point_token = db.Column(db.String(255), default="", nullable=True)  # Store encrypted in production
    peppol_access_point_timeout = db.Column(db.Integer, default=30, nullable=True)
    peppol_provider = db.Column(db.String(50), default="generic", nullable=True)
    # Transport: generic (HTTP JSON AP) or native (SML/SMP + AS4)
    peppol_transport_mode = db.Column(db.String(20), default="generic", nullable=True)
    peppol_sml_url = db.Column(db.String(500), default="", nullable=True)
    peppol_native_cert_path = db.Column(db.String(500), default="", nullable=True)
    peppol_native_key_path = db.Column(db.String(500), default="", nullable=True)
    invoices_peppol_compliant = db.Column(db.Boolean, default=False, nullable=False)
    # When True, exported invoice PDFs embed EN 16931 UBL XML (ZugFerd/Factur-X)
    invoices_zugferd_pdf = db.Column(db.Boolean, default=False, nullable=False)
    # When True and ZUGFeRD is on, export is normalized to PDF/A-3 for validators
    invoices_pdfa3_compliant = db.Column(db.Boolean, default=False, nullable=False)
    # Optional: run veraPDF after export and show summary (does not block export)
    invoices_validate_export = db.Column(db.Boolean, default=False, nullable=False)
    invoices_verapdf_path = db.Column(db.String(500), default="", nullable=True)

    # Privacy and analytics settings
    allow_analytics = db.Column(db.Boolean, default=True, nullable=False)  # Controls system info sharing for analytics

    # Module visibility: admin-disabled module IDs (e.g. ["gantt", "leads"]). Empty/None = all enabled.
    disabled_module_ids = db.Column(db.JSON, default=list, nullable=True)

    # Optional: lock the app to a single client (company-only usage).
    # When set, the UI should auto-select this client and prevent changes.
    locked_client_id = db.Column(db.Integer, nullable=True)

    # Stable per-installation ID (UUID); used for donate-hide code requests.
    system_instance_id = db.Column(db.String(36), nullable=True)
    # When True, donate/support UI is hidden for all users (set after code verification in Admin).
    donate_ui_hidden = db.Column(db.Boolean, default=False, nullable=False)

    # Kiosk mode settings
    kiosk_mode_enabled = db.Column(db.Boolean, default=False, nullable=False)
    kiosk_auto_logout_minutes = db.Column(db.Integer, default=15, nullable=False)
    kiosk_allow_camera_scanning = db.Column(db.Boolean, default=True, nullable=False)
    kiosk_require_reason_for_adjustments = db.Column(db.Boolean, default=False, nullable=False)
    kiosk_default_movement_type = db.Column(db.String(20), default="adjustment", nullable=False)

    # Time entry requirements (admin-enforced when logging time)
    time_entry_require_task = db.Column(db.Boolean, default=False, nullable=False)
    time_entry_require_description = db.Column(db.Boolean, default=False, nullable=False)
    time_entry_description_min_length = db.Column(db.Integer, default=20, nullable=False)

    # AI helper provider configuration. API keys stay server-side and are never serialized.
    ai_enabled = db.Column(db.Boolean, default=None, nullable=True)
    ai_provider = db.Column(db.String(50), default="", nullable=True)
    ai_base_url = db.Column(db.String(500), default="", nullable=True)
    ai_model = db.Column(db.String(120), default="", nullable=True)
    ai_api_key = db.Column(db.String(500), default="", nullable=True)
    ai_timeout_seconds = db.Column(db.Integer, default=None, nullable=True)
    ai_context_limit = db.Column(db.Integer, default=None, nullable=True)
    ai_system_prompt = db.Column(db.Text, default="", nullable=True)

    # Overtime / time tracking: default daily working hours for new users (e.g. 8.0)
    default_daily_working_hours = db.Column(db.Float, default=8.0, nullable=False)

    # Default break rules for time entries (e.g. Germany: >6h = 30 min, >9h = 45 min). User can override per entry.
    break_after_hours_1 = db.Column(db.Float, nullable=True)  # e.g. 6
    break_minutes_1 = db.Column(db.Integer, nullable=True)  # e.g. 30
    break_after_hours_2 = db.Column(db.Float, nullable=True)  # e.g. 9
    break_minutes_2 = db.Column(db.Integer, nullable=True)  # e.g. 45

    # Email configuration settings (stored in database, takes precedence over environment variables)
    mail_enabled = db.Column(db.Boolean, default=False, nullable=False)  # Enable database-backed email config
    mail_server = db.Column(db.String(255), default="", nullable=True)
    mail_port = db.Column(db.Integer, default=587, nullable=True)
    mail_use_tls = db.Column(db.Boolean, default=True, nullable=True)
    mail_use_ssl = db.Column(db.Boolean, default=False, nullable=True)
    mail_username = db.Column(db.String(255), default="", nullable=True)
    mail_password = db.Column(db.String(255), default="", nullable=True)  # Store encrypted in production
    mail_default_sender = db.Column(db.String(255), default="", nullable=True)
    mail_test_recipient = db.Column(db.String(255), default="", nullable=True)

    # Integration OAuth credentials (stored in database, takes precedence over environment variables)
    # Jira
    jira_client_id = db.Column(db.String(255), default="", nullable=True)
    jira_client_secret = db.Column(db.String(255), default="", nullable=True)  # Store encrypted in production
    # Slack
    slack_client_id = db.Column(db.String(255), default="", nullable=True)
    slack_client_secret = db.Column(db.String(255), default="", nullable=True)  # Store encrypted in production
    # GitHub
    github_client_id = db.Column(db.String(255), default="", nullable=True)
    github_client_secret = db.Column(db.String(255), default="", nullable=True)  # Store encrypted in production
    # Google Calendar
    google_calendar_client_id = db.Column(db.String(255), default="", nullable=True)
    google_calendar_client_secret = db.Column(
        db.String(255), default="", nullable=True
    )  # Store encrypted in production
    # Outlook Calendar
    outlook_calendar_client_id = db.Column(db.String(255), default="", nullable=True)
    outlook_calendar_client_secret = db.Column(
        db.String(255), default="", nullable=True
    )  # Store encrypted in production
    outlook_calendar_tenant_id = db.Column(db.String(255), default="", nullable=True)
    # Microsoft Teams
    microsoft_teams_client_id = db.Column(db.String(255), default="", nullable=True)
    microsoft_teams_client_secret = db.Column(
        db.String(255), default="", nullable=True
    )  # Store encrypted in production
    microsoft_teams_tenant_id = db.Column(db.String(255), default="", nullable=True)
    # Asana
    asana_client_id = db.Column(db.String(255), default="", nullable=True)
    asana_client_secret = db.Column(db.String(255), default="", nullable=True)  # Store encrypted in production
    # Trello
    trello_api_key = db.Column(db.String(255), default="", nullable=True)
    trello_api_secret = db.Column(db.String(255), default="", nullable=True)  # Store encrypted in production
    # GitLab
    gitlab_client_id = db.Column(db.String(255), default="", nullable=True)
    gitlab_client_secret = db.Column(db.String(255), default="", nullable=True)  # Store encrypted in production
    gitlab_instance_url = db.Column(db.String(500), default="", nullable=True)
    # QuickBooks
    quickbooks_client_id = db.Column(db.String(255), default="", nullable=True)
    quickbooks_client_secret = db.Column(db.String(255), default="", nullable=True)  # Store encrypted in production
    # Xero
    xero_client_id = db.Column(db.String(255), default="", nullable=True)
    xero_client_secret = db.Column(db.String(255), default="", nullable=True)  # Store encrypted in production

    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)

    def __init__(self, **kwargs):
        # Set defaults from config
        self.timezone = kwargs.get("timezone", Config.TZ)
        self.date_format = kwargs.get("date_format", "YYYY-MM-DD")
        self.time_format = kwargs.get("time_format", "24h")
        self.currency = kwargs.get("currency", Config.CURRENCY)
        self.rounding_minutes = kwargs.get("rounding_minutes", Config.ROUNDING_MINUTES)
        self.single_active_timer = kwargs.get("single_active_timer", Config.SINGLE_ACTIVE_TIMER)
        self.allow_self_register = kwargs.get("allow_self_register", Config.ALLOW_SELF_REGISTER)
        self.idle_timeout_minutes = kwargs.get("idle_timeout_minutes", Config.IDLE_TIMEOUT_MINUTES)
        self.backup_retention_days = kwargs.get("backup_retention_days", Config.BACKUP_RETENTION_DAYS)
        self.backup_time = kwargs.get("backup_time", Config.BACKUP_TIME)
        self.export_delimiter = kwargs.get("export_delimiter", ",")

        # Set company branding defaults
        self.company_name = kwargs.get("company_name", "Your Company Name")
        self.company_address = kwargs.get("company_address", "Your Company Address")
        self.company_email = kwargs.get("company_email", "info@yourcompany.com")
        self.company_phone = kwargs.get("company_phone", "+1 (555) 123-4567")
        self.company_website = kwargs.get("company_website", "www.yourcompany.com")
        self.company_logo_filename = kwargs.get("company_logo_filename", "")
        self.company_tax_id = kwargs.get("company_tax_id", "")
        self.company_bank_info = kwargs.get("company_bank_info", "")

        # PDF template customization
        self.invoice_pdf_template_html = kwargs.get("invoice_pdf_template_html", "")
        self.invoice_pdf_template_css = kwargs.get("invoice_pdf_template_css", "")
        self.invoice_pdf_design_json = kwargs.get("invoice_pdf_design_json", "")

        # Set invoice defaults
        self.invoice_prefix = kwargs.get("invoice_prefix", "INV")
        self.invoice_number_pattern = kwargs.get("invoice_number_pattern", DEFAULT_INVOICE_PATTERN)
        self.invoice_start_number = kwargs.get("invoice_start_number", 1000)
        self.invoice_terms = kwargs.get("invoice_terms", "Payment is due within 30 days of invoice date.")
        self.invoice_notes = kwargs.get("invoice_notes", "Thank you for your business!")

        # Peppol defaults (None means "use env var")
        self.peppol_enabled = kwargs.get("peppol_enabled", None)
        self.peppol_sender_endpoint_id = kwargs.get("peppol_sender_endpoint_id", "")
        self.peppol_sender_scheme_id = kwargs.get("peppol_sender_scheme_id", "")
        self.peppol_sender_country = kwargs.get("peppol_sender_country", "")
        self.peppol_access_point_url = kwargs.get("peppol_access_point_url", "")
        self.peppol_access_point_token = kwargs.get("peppol_access_point_token", "")
        self.peppol_access_point_timeout = kwargs.get("peppol_access_point_timeout", 30)
        self.peppol_provider = kwargs.get("peppol_provider", "generic")
        self.peppol_transport_mode = kwargs.get("peppol_transport_mode", "generic")
        self.peppol_sml_url = kwargs.get("peppol_sml_url", "")
        self.peppol_native_cert_path = kwargs.get("peppol_native_cert_path", "")
        self.peppol_native_key_path = kwargs.get("peppol_native_key_path", "")
        self.invoices_peppol_compliant = kwargs.get("invoices_peppol_compliant", False)
        self.invoices_zugferd_pdf = kwargs.get("invoices_zugferd_pdf", False)
        self.invoices_pdfa3_compliant = kwargs.get("invoices_pdfa3_compliant", False)
        self.invoices_validate_export = kwargs.get("invoices_validate_export", False)
        self.invoices_verapdf_path = kwargs.get("invoices_verapdf_path", "")

        # Kiosk mode defaults
        self.kiosk_mode_enabled = kwargs.get("kiosk_mode_enabled", False)
        self.kiosk_auto_logout_minutes = kwargs.get("kiosk_auto_logout_minutes", 15)
        self.kiosk_allow_camera_scanning = kwargs.get("kiosk_allow_camera_scanning", True)
        self.kiosk_require_reason_for_adjustments = kwargs.get("kiosk_require_reason_for_adjustments", False)
        self.kiosk_default_movement_type = kwargs.get("kiosk_default_movement_type", "adjustment")

        # Email configuration defaults
        self.mail_enabled = kwargs.get("mail_enabled", False)
        self.mail_server = kwargs.get("mail_server", "")
        self.mail_port = kwargs.get("mail_port", 587)
        self.mail_use_tls = kwargs.get("mail_use_tls", True)
        self.mail_use_ssl = kwargs.get("mail_use_ssl", False)
        self.mail_username = kwargs.get("mail_username", "")
        self.mail_password = kwargs.get("mail_password", "")
        self.mail_default_sender = kwargs.get("mail_default_sender", "")
        self.mail_test_recipient = kwargs.get("mail_test_recipient", "")

        # AI helper defaults. None/empty values fall back to environment/app config.
        self.ai_enabled = kwargs.get("ai_enabled", None)
        self.ai_provider = kwargs.get("ai_provider", "")
        self.ai_base_url = kwargs.get("ai_base_url", "")
        self.ai_model = kwargs.get("ai_model", "")
        self.ai_api_key = kwargs.get("ai_api_key", "")
        self.ai_timeout_seconds = kwargs.get("ai_timeout_seconds", None)
        self.ai_context_limit = kwargs.get("ai_context_limit", None)
        self.ai_system_prompt = kwargs.get("ai_system_prompt", "")

        # Integration OAuth credentials defaults
        self.jira_client_id = kwargs.get("jira_client_id", "")
        self.jira_client_secret = kwargs.get("jira_client_secret", "")
        self.slack_client_id = kwargs.get("slack_client_id", "")
        self.slack_client_secret = kwargs.get("slack_client_secret", "")
        self.github_client_id = kwargs.get("github_client_id", "")
        self.github_client_secret = kwargs.get("github_client_secret", "")
        self.google_calendar_client_id = kwargs.get("google_calendar_client_id", "")
        self.google_calendar_client_secret = kwargs.get("google_calendar_client_secret", "")
        self.outlook_calendar_client_id = kwargs.get("outlook_calendar_client_id", "")
        self.outlook_calendar_client_secret = kwargs.get("outlook_calendar_client_secret", "")
        self.outlook_calendar_tenant_id = kwargs.get("outlook_calendar_tenant_id", "")
        self.microsoft_teams_client_id = kwargs.get("microsoft_teams_client_id", "")
        self.microsoft_teams_client_secret = kwargs.get("microsoft_teams_client_secret", "")
        self.microsoft_teams_tenant_id = kwargs.get("microsoft_teams_tenant_id", "")
        self.asana_client_id = kwargs.get("asana_client_id", "")
        self.asana_client_secret = kwargs.get("asana_client_secret", "")
        self.trello_api_key = kwargs.get("trello_api_key", "")
        self.trello_api_secret = kwargs.get("trello_api_secret", "")
        self.gitlab_client_id = kwargs.get("gitlab_client_id", "")
        self.gitlab_client_secret = kwargs.get("gitlab_client_secret", "")
        self.gitlab_instance_url = kwargs.get("gitlab_instance_url", "")
        self.quickbooks_client_id = kwargs.get("quickbooks_client_id", "")
        self.quickbooks_client_secret = kwargs.get("quickbooks_client_secret", "")
        self.xero_client_id = kwargs.get("xero_client_id", "")
        self.xero_client_secret = kwargs.get("xero_client_secret", "")

    def __repr__(self):
        return f"<Settings {self.id}>"

    def get_logo_url(self):
        """Get the full URL for the company logo"""
        if self.company_logo_filename:
            return f"/uploads/logos/{self.company_logo_filename}"
        return None

    def get_logo_path(self):
        """Get the full file system path for the company logo"""
        if not self.company_logo_filename:
            return None

        try:
            from flask import current_app

            upload_folder = os.path.join(current_app.root_path, "static", "uploads", "logos")
            return os.path.join(upload_folder, self.company_logo_filename)
        except RuntimeError:
            # current_app not available (e.g., during testing or initialization)
            # Fallback to a relative path
            return os.path.join("app", "static", "uploads", "logos", self.company_logo_filename)

    def has_logo(self):
        """Check if company has a logo uploaded"""
        if not self.company_logo_filename:
            return False

        logo_path = self.get_logo_path()
        return logo_path and os.path.exists(logo_path)

    def get_mail_config(self):
        """Get email configuration, preferring database settings over environment variables"""
        if self.mail_enabled and self.mail_server:
            return {
                "MAIL_SERVER": self.mail_server,
                "MAIL_PORT": self.mail_port or 587,
                "MAIL_USE_TLS": self.mail_use_tls if self.mail_use_tls is not None else True,
                "MAIL_USE_SSL": self.mail_use_ssl if self.mail_use_ssl is not None else False,
                "MAIL_USERNAME": self.mail_username or None,
                "MAIL_PASSWORD": (decrypt_if_needed(self.mail_password) or None),
                "MAIL_DEFAULT_SENDER": self.mail_default_sender or "noreply@timetracker.local",
            }
        return None

    def get_ai_config(self, *, include_secrets: bool = False) -> dict:
        """Get AI helper configuration, preferring database settings over environment/app config.

        By default, secrets are not returned (UI-safe). Use include_secrets=True for server runtime calls.
        """
        from flask import current_app

        def cfg(name, default=None):
            try:
                return current_app.config.get(name, default)
            except RuntimeError:
                return getattr(Config, name, default)

        provider = (getattr(self, "ai_provider", "") or cfg("AI_PROVIDER", "ollama") or "ollama").strip().lower()
        base_url = (getattr(self, "ai_base_url", "") or cfg("AI_BASE_URL", "http://127.0.0.1:11434") or "").strip()
        model = (getattr(self, "ai_model", "") or cfg("AI_MODEL", "llama3.1") or "").strip()
        timeout = getattr(self, "ai_timeout_seconds", None) or cfg("AI_TIMEOUT_SECONDS", 30)
        context_limit = getattr(self, "ai_context_limit", None) or cfg("AI_CONTEXT_LIMIT", 40)
        system_prompt = (getattr(self, "ai_system_prompt", "") or cfg("AI_SYSTEM_PROMPT", "") or "").strip()
        api_key_raw = (getattr(self, "ai_api_key", "") or cfg("AI_API_KEY", "") or "").strip()
        api_key = decrypt_if_needed(api_key_raw) if api_key_raw else ""
        enabled = getattr(self, "ai_enabled", None)
        if enabled is None:
            enabled = bool(cfg("AI_ENABLED", False))

        try:
            timeout = max(1, int(timeout))
        except (TypeError, ValueError):
            timeout = 30
        try:
            context_limit = max(5, int(context_limit))
        except (TypeError, ValueError):
            context_limit = 40

        return {
            "enabled": bool(enabled),
            "provider": provider if provider in {"ollama", "openai_compatible"} else "ollama",
            "base_url": base_url.rstrip("/"),
            "model": model,
            "api_key": api_key if include_secrets else "",
            "api_key_set": bool(api_key_raw),
            "timeout_seconds": timeout,
            "context_limit": context_limit,
            "system_prompt": system_prompt,
        }

    def get_integration_credentials(self, provider: str, *, include_secrets: bool = True) -> dict:
        """Get integration OAuth credentials, preferring database settings over environment variables.

        Args:
            provider: One of 'jira', 'slack', 'github', 'google_calendar', 'outlook_calendar',
                     'microsoft_teams', 'asana', 'trello', 'gitlab', 'quickbooks', 'xero'

        Returns:
            dict with credentials (varies by provider):
            - Standard OAuth: 'client_id', 'client_secret'
            - Microsoft: 'client_id', 'client_secret', 'tenant_id'
            - Trello: 'api_key', 'api_secret'
            - GitLab: 'client_id', 'client_secret', 'instance_url'
        """
        import os

        if provider == "jira":
            client_id = self.jira_client_id or os.getenv("JIRA_CLIENT_ID", "")
            client_secret_raw = self.jira_client_secret or os.getenv("JIRA_CLIENT_SECRET", "")
            client_secret = decrypt_if_needed(client_secret_raw) if include_secrets else ""
            return {"client_id": client_id, "client_secret": client_secret}

        elif provider == "slack":
            client_id = self.slack_client_id or os.getenv("SLACK_CLIENT_ID", "")
            client_secret_raw = self.slack_client_secret or os.getenv("SLACK_CLIENT_SECRET", "")
            client_secret = decrypt_if_needed(client_secret_raw) if include_secrets else ""
            return {"client_id": client_id, "client_secret": client_secret}

        elif provider == "github":
            client_id = self.github_client_id or os.getenv("GITHUB_CLIENT_ID", "")
            client_secret_raw = self.github_client_secret or os.getenv("GITHUB_CLIENT_SECRET", "")
            client_secret = decrypt_if_needed(client_secret_raw) if include_secrets else ""
            return {"client_id": client_id, "client_secret": client_secret}

        elif provider == "google_calendar":
            client_id = getattr(self, "google_calendar_client_id", "") or os.getenv("GOOGLE_CLIENT_ID", "")
            client_secret_raw = getattr(self, "google_calendar_client_secret", "") or os.getenv("GOOGLE_CLIENT_SECRET", "")
            client_secret = decrypt_if_needed(client_secret_raw) if include_secrets else ""
            return {"client_id": client_id, "client_secret": client_secret}

        elif provider == "outlook_calendar":
            client_id = getattr(self, "outlook_calendar_client_id", "") or os.getenv("OUTLOOK_CLIENT_ID", "")
            client_secret_raw = getattr(self, "outlook_calendar_client_secret", "") or os.getenv(
                "OUTLOOK_CLIENT_SECRET", ""
            )
            tenant_id = getattr(self, "outlook_calendar_tenant_id", "") or os.getenv("OUTLOOK_TENANT_ID", "")
            client_secret = decrypt_if_needed(client_secret_raw) if include_secrets else ""
            return {"client_id": client_id, "client_secret": client_secret, "tenant_id": tenant_id}

        elif provider == "microsoft_teams":
            client_id = getattr(self, "microsoft_teams_client_id", "") or os.getenv("MICROSOFT_TEAMS_CLIENT_ID", "")
            client_secret_raw = getattr(self, "microsoft_teams_client_secret", "") or os.getenv(
                "MICROSOFT_TEAMS_CLIENT_SECRET", ""
            )
            tenant_id = getattr(self, "microsoft_teams_tenant_id", "") or os.getenv("MICROSOFT_TEAMS_TENANT_ID", "")
            client_secret = decrypt_if_needed(client_secret_raw) if include_secrets else ""
            return {"client_id": client_id, "client_secret": client_secret, "tenant_id": tenant_id}

        elif provider == "asana":
            client_id = getattr(self, "asana_client_id", "") or os.getenv("ASANA_CLIENT_ID", "")
            client_secret_raw = getattr(self, "asana_client_secret", "") or os.getenv("ASANA_CLIENT_SECRET", "")
            client_secret = decrypt_if_needed(client_secret_raw) if include_secrets else ""
            return {"client_id": client_id, "client_secret": client_secret}

        elif provider == "trello":
            api_key = getattr(self, "trello_api_key", "") or os.getenv("TRELLO_API_KEY", "")
            api_secret_raw = getattr(self, "trello_api_secret", "") or os.getenv("TRELLO_API_SECRET", "")
            api_secret = decrypt_if_needed(api_secret_raw) if include_secrets else ""
            return {"api_key": api_key, "api_secret": api_secret}

        elif provider == "gitlab":
            client_id = getattr(self, "gitlab_client_id", "") or os.getenv("GITLAB_CLIENT_ID", "")
            client_secret_raw = getattr(self, "gitlab_client_secret", "") or os.getenv("GITLAB_CLIENT_SECRET", "")
            instance_url = getattr(self, "gitlab_instance_url", "") or os.getenv(
                "GITLAB_INSTANCE_URL", "https://gitlab.com"
            )
            client_secret = decrypt_if_needed(client_secret_raw) if include_secrets else ""
            return {"client_id": client_id, "client_secret": client_secret, "instance_url": instance_url}

        elif provider == "quickbooks":
            client_id = getattr(self, "quickbooks_client_id", "") or os.getenv("QUICKBOOKS_CLIENT_ID", "")
            client_secret_raw = getattr(self, "quickbooks_client_secret", "") or os.getenv("QUICKBOOKS_CLIENT_SECRET", "")
            client_secret = decrypt_if_needed(client_secret_raw) if include_secrets else ""
            return {"client_id": client_id, "client_secret": client_secret}

        elif provider == "xero":
            client_id = getattr(self, "xero_client_id", "") or os.getenv("XERO_CLIENT_ID", "")
            client_secret_raw = getattr(self, "xero_client_secret", "") or os.getenv("XERO_CLIENT_SECRET", "")
            client_secret = decrypt_if_needed(client_secret_raw) if include_secrets else ""
            return {"client_id": client_id, "client_secret": client_secret}

        else:
            return {}

    def to_dict(self):
        """Convert settings to dictionary for API responses"""
        return {
            "id": self.id,
            "timezone": self.timezone,
            "date_format": self.date_format,
            "time_format": self.time_format,
            "currency": self.currency,
            "rounding_minutes": self.rounding_minutes,
            "single_active_timer": self.single_active_timer,
            "allow_self_register": self.allow_self_register,
            "idle_timeout_minutes": self.idle_timeout_minutes,
            "backup_retention_days": self.backup_retention_days,
            "backup_time": self.backup_time,
            "export_delimiter": self.export_delimiter,
            "company_name": self.company_name,
            "company_address": self.company_address,
            "company_email": self.company_email,
            "company_phone": self.company_phone,
            "company_website": self.company_website,
            "company_logo_filename": self.company_logo_filename,
            "company_logo_url": self.get_logo_url(),
            "has_logo": self.has_logo(),
            "company_tax_id": self.company_tax_id,
            "company_bank_info": self.company_bank_info,
            "invoice_prefix": self.invoice_prefix,
            "invoice_number_pattern": self.invoice_number_pattern,
            "invoice_start_number": self.invoice_start_number,
            "invoice_terms": self.invoice_terms,
            "invoice_notes": self.invoice_notes,
            "peppol_enabled": self.peppol_enabled,
            "peppol_sender_endpoint_id": getattr(self, "peppol_sender_endpoint_id", "") or "",
            "peppol_sender_scheme_id": getattr(self, "peppol_sender_scheme_id", "") or "",
            "peppol_sender_country": getattr(self, "peppol_sender_country", "") or "",
            "peppol_access_point_url": getattr(self, "peppol_access_point_url", "") or "",
            "peppol_access_point_token_set": bool(getattr(self, "peppol_access_point_token", "")),
            "peppol_access_point_timeout": getattr(self, "peppol_access_point_timeout", None),
            "peppol_provider": getattr(self, "peppol_provider", "") or "",
            "peppol_transport_mode": getattr(self, "peppol_transport_mode", "") or "generic",
            "peppol_sml_url": getattr(self, "peppol_sml_url", "") or "",
            "peppol_native_cert_path": getattr(self, "peppol_native_cert_path", "") or "",
            "peppol_native_key_path": getattr(self, "peppol_native_key_path", "") or "",
            "invoices_peppol_compliant": getattr(self, "invoices_peppol_compliant", False),
            "invoices_zugferd_pdf": getattr(self, "invoices_zugferd_pdf", False),
            "invoices_pdfa3_compliant": getattr(self, "invoices_pdfa3_compliant", False),
            "invoices_validate_export": getattr(self, "invoices_validate_export", False),
            "invoices_verapdf_path": getattr(self, "invoices_verapdf_path", "") or "",
            "invoice_pdf_template_html": self.invoice_pdf_template_html,
            "invoice_pdf_template_css": self.invoice_pdf_template_css,
            "invoice_pdf_design_json": self.invoice_pdf_design_json,
            "allow_analytics": self.allow_analytics,
            "disabled_module_ids": (self.disabled_module_ids if self.disabled_module_ids is not None else []),
            "locked_client_id": getattr(self, "locked_client_id", None),
            "mail_enabled": self.mail_enabled,
            "mail_server": self.mail_server,
            "mail_port": self.mail_port,
            "mail_use_tls": self.mail_use_tls,
            "mail_use_ssl": self.mail_use_ssl,
            "mail_username": self.mail_username,
            "mail_password_set": bool(self.mail_password),  # Don't expose actual password
            "mail_default_sender": self.mail_default_sender,
            "mail_test_recipient": getattr(self, "mail_test_recipient", "") or "",
            "jira_client_id": self.jira_client_id or "",
            "jira_client_secret_set": bool(self.jira_client_secret),  # Don't expose actual secret
            "slack_client_id": self.slack_client_id or "",
            "slack_client_secret_set": bool(self.slack_client_secret),  # Don't expose actual secret
            "github_client_id": self.github_client_id or "",
            "github_client_secret_set": bool(self.github_client_secret),  # Don't expose actual secret
            "google_calendar_client_id": getattr(self, "google_calendar_client_id", "") or "",
            "google_calendar_client_secret_set": bool(getattr(self, "google_calendar_client_secret", "")),
            "outlook_calendar_client_id": getattr(self, "outlook_calendar_client_id", "") or "",
            "outlook_calendar_client_secret_set": bool(getattr(self, "outlook_calendar_client_secret", "")),
            "outlook_calendar_tenant_id": getattr(self, "outlook_calendar_tenant_id", "") or "",
            "microsoft_teams_client_id": getattr(self, "microsoft_teams_client_id", "") or "",
            "microsoft_teams_client_secret_set": bool(getattr(self, "microsoft_teams_client_secret", "")),
            "microsoft_teams_tenant_id": getattr(self, "microsoft_teams_tenant_id", "") or "",
            "asana_client_id": getattr(self, "asana_client_id", "") or "",
            "asana_client_secret_set": bool(getattr(self, "asana_client_secret", "")),
            "trello_api_key": getattr(self, "trello_api_key", "") or "",
            "trello_api_secret_set": bool(getattr(self, "trello_api_secret", "")),
            "gitlab_client_id": getattr(self, "gitlab_client_id", "") or "",
            "gitlab_client_secret_set": bool(getattr(self, "gitlab_client_secret", "")),
            "gitlab_instance_url": getattr(self, "gitlab_instance_url", "") or "",
            "quickbooks_client_id": getattr(self, "quickbooks_client_id", "") or "",
            "quickbooks_client_secret_set": bool(getattr(self, "quickbooks_client_secret", "")),
            "xero_client_id": getattr(self, "xero_client_id", "") or "",
            "xero_client_secret_set": bool(getattr(self, "xero_client_secret", "")),
            "time_entry_require_task": getattr(self, "time_entry_require_task", False),
            "time_entry_require_description": getattr(self, "time_entry_require_description", False),
            "time_entry_description_min_length": getattr(self, "time_entry_description_min_length", 20),
            "default_daily_working_hours": float(getattr(self, "default_daily_working_hours", 8.0) or 8.0),
            "ai_enabled": getattr(self, "ai_enabled", None),
            "ai_provider": getattr(self, "ai_provider", "") or "",
            "ai_base_url": getattr(self, "ai_base_url", "") or "",
            "ai_model": getattr(self, "ai_model", "") or "",
            "ai_api_key_set": bool(getattr(self, "ai_api_key", "")),
            "ai_timeout_seconds": getattr(self, "ai_timeout_seconds", None),
            "ai_context_limit": getattr(self, "ai_context_limit", None),
            "ai_system_prompt": getattr(self, "ai_system_prompt", "") or "",
            "created_at": self.created_at.isoformat() if self.created_at else None,
            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
        }

    _SECRET_FIELDS = (
        "mail_password",
        "peppol_access_point_token",
        "ai_api_key",
        "jira_client_secret",
        "slack_client_secret",
        "github_client_secret",
        "google_calendar_client_secret",
        "outlook_calendar_client_secret",
        "microsoft_teams_client_secret",
        "asana_client_secret",
        "trello_api_secret",
        "gitlab_client_secret",
        "quickbooks_client_secret",
        "xero_client_secret",
    )

    def set_secret(self, field: str, value: str) -> None:
        if field not in self._SECRET_FIELDS:
            raise ValueError("unsupported secret field")
        value = (value or "").strip()
        if not value:
            setattr(self, field, "")
            return
        if secrets_encryption_configured():
            setattr(self, field, encrypt_if_possible(value))
        else:
            # Best-effort fallback; still store (legacy behavior) but mark clearly in logs by leaving plaintext.
            setattr(self, field, value)

    def get_secret(self, field: str) -> str:
        if field not in self._SECRET_FIELDS:
            raise ValueError("unsupported secret field")
        return decrypt_if_needed(getattr(self, field, "") or "")

    def _encrypt_secrets_if_needed(self) -> bool:
        """
        One-time best-effort migration: if a secret is stored in plaintext and encryption is configured,
        rewrite it encrypted. Returns True if any field changed.
        """
        if not secrets_encryption_configured():
            return False
        changed = False
        for f in self._SECRET_FIELDS:
            raw = (getattr(self, f, "") or "").strip()
            if raw and not raw.startswith("enc:v1:"):
                setattr(self, f, encrypt_if_possible(raw))
                changed = True
        return changed

    @classmethod
    def get_settings(cls):
        """Get the singleton settings instance, creating it if it doesn't exist.

        When creating a new Settings instance, it will be initialized from
        environment variables (.env file) as initial values.
        """
        try:
            settings = cls.query.first()
            # #region agent log
            try:
                import json

                log_data = {
                    "location": "settings.py:422",
                    "message": "Settings query result",
                    "data": {
                        "settings_is_none": settings is None,
                        "settings_has_id": settings is not None and hasattr(settings, "id") and settings.id is not None,
                        "invoice_prefix": getattr(settings, "invoice_prefix", "MISSING") if settings else "N/A",
                        "invoice_start_number": (
                            getattr(settings, "invoice_start_number", "MISSING") if settings else "N/A"
                        ),
                    },
                    "timestamp": int(datetime.utcnow().timestamp() * 1000),
                    "sessionId": "debug-session",
                    "runId": "run1",
                    "hypothesisId": "D",
                }
                log_path = os.path.join(
                    os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".cursor", "debug.log"
                )
                with open(log_path, "a", encoding="utf-8") as f:
                    f.write(json.dumps(log_data) + "\n")
            except (OSError, IOError, TypeError, ValueError):
                pass
            # #endregion
            if settings:
                return settings
        except Exception as e:
            # Handle case where table or columns don't exist yet (migration not run)
            # Check if it's a table/column error - if so, it's expected during migrations
            error_str = str(e)
            # Also check the underlying exception if it's a SQLAlchemy exception
            underlying_error = ""
            if hasattr(e, "orig"):
                underlying_error = str(e.orig)
            elif hasattr(e, "__cause__") and e.__cause__:
                underlying_error = str(e.__cause__)

            combined_error = f"{error_str} {underlying_error}".lower()
            is_schema_error = (
                "undefinedcolumn" in combined_error
                or "does not exist" in combined_error
                or "no such column" in combined_error
                or "no such table" in combined_error
                or ("relation" in combined_error and "does not exist" in combined_error)
                or "operationalerror" in combined_error
                and ("no such table" in combined_error or "does not exist" in combined_error)
            )

            import logging

            logger = logging.getLogger(__name__)

            if is_schema_error:
                # This is expected during migrations when schema is incomplete
                # Only log at debug level to avoid cluttering logs
                logger.debug(
                    f"Settings table not available (migration may be pending): {error_str.split('LINE')[0] if 'LINE' in error_str else error_str}"
                )
            else:
                # Other errors should be logged as warnings
                logger.warning(f"Could not query settings: {e}")

            # Rollback the failed transaction
            try:
                db.session.rollback()
            except Exception:
                pass
            # Return fallback instance with defaults
            return cls()

        # Avoid performing session writes during flush/commit phases.
        # When called from default column factories or listeners during flush,
        # SQLAlchemy may be in the middle of a flush. Writing here would raise
        # SAWarnings/ResourceClosedError. Skip add+commit and return a transient
        # instance; the persistent row can be created later by init or admin flows.
        try:
            if getattr(_creating_settings, "active", False):
                return cls()
            if _session_in_flush(db.session):
                return cls()
            try:
                _creating_settings.active = True
                # Create new settings instance initialized from environment variables
                settings = cls()
                # Initialize from environment variables (.env file)
                cls._initialize_from_env(settings)
                db.session.add(settings)
                db.session.commit()
                return settings
            finally:
                _creating_settings.active = False
        except Exception:
            # If anything goes wrong creating the persistent row, rollback and
            # fall back to an in-memory Settings instance.
            try:
                db.session.rollback()
            except Exception:
                # Ignore rollback failures to avoid masking original contexts
                pass

        # Fallback: return a non-persisted Settings instance
        import logging

        logging.getLogger(__name__).warning(
            "Returning transient in-memory Settings instance (database row missing or creation failed). "
            "Check database connectivity and migrations."
        )
        return cls()

    @classmethod
    def update_settings(cls, **kwargs):
        """Update settings with new values"""
        settings = cls.get_settings()

        for key, value in kwargs.items():
            if hasattr(settings, key):
                setattr(settings, key, value)

        settings.updated_at = datetime.utcnow()
        db.session.commit()
        # Best-effort migration of plaintext secrets to encrypted-at-rest when key is configured.
        try:
            if settings and settings._encrypt_secrets_if_needed() and not _session_in_flush(db.session):
                db.session.add(settings)
                db.session.commit()
        except Exception:
            try:
                db.session.rollback()
            except Exception:
                pass
        return settings

    @classmethod
    def get_system_instance_id(cls):
        """Return stable per-installation UUID; create and persist if missing."""
        import uuid

        settings = cls.get_settings()
        if not getattr(settings, "id", None):
            return None
        if getattr(settings, "system_instance_id", None):
            return settings.system_instance_id
        try:
            if _session_in_flush(db.session):
                return None
            settings.system_instance_id = str(uuid.uuid4())
            db.session.commit()
            return settings.system_instance_id
        except Exception:
            try:
                db.session.rollback()
            except Exception:
                pass
            return None

    @classmethod
    def _initialize_from_env(cls, settings_instance):
        """
        Initialize Settings instance from environment variables (.env file).
        This is called when creating a new Settings instance to use .env values
        as initial startup values.

        Args:
            settings_instance: Settings instance to initialize
        """
        # Map environment variable names to Settings model attributes
        env_mapping = {
            "TZ": "timezone",
            "DATE_FORMAT": "date_format",
            "TIME_FORMAT": "time_format",
            "CURRENCY": "currency",
            "ROUNDING_MINUTES": "rounding_minutes",
            "SINGLE_ACTIVE_TIMER": "single_active_timer",
            "ALLOW_SELF_REGISTER": "allow_self_register",
            "IDLE_TIMEOUT_MINUTES": "idle_timeout_minutes",
            "BACKUP_RETENTION_DAYS": "backup_retention_days",
            "BACKUP_TIME": "backup_time",
            "DEFAULT_DAILY_WORKING_HOURS": "default_daily_working_hours",
            "AI_ENABLED": "ai_enabled",
            "AI_PROVIDER": "ai_provider",
            "AI_BASE_URL": "ai_base_url",
            "AI_MODEL": "ai_model",
            "AI_API_KEY": "ai_api_key",
            "AI_TIMEOUT_SECONDS": "ai_timeout_seconds",
            "AI_CONTEXT_LIMIT": "ai_context_limit",
            "AI_SYSTEM_PROMPT": "ai_system_prompt",
        }

        for env_var, attr_name in env_mapping.items():
            if hasattr(settings_instance, attr_name):
                env_value = os.getenv(env_var)
                if env_value is not None:
                    # Convert value types based on attribute type
                    current_value = getattr(settings_instance, attr_name)

                    if isinstance(current_value, bool):
                        # Handle boolean values
                        setattr(settings_instance, attr_name, env_value.lower() == "true")
                    elif isinstance(current_value, int):
                        # Handle integer values
                        try:
                            setattr(settings_instance, attr_name, int(env_value))
                        except (ValueError, TypeError):
                            pass  # Keep default if conversion fails
                    elif isinstance(current_value, float):
                        try:
                            setattr(settings_instance, attr_name, float(env_value))
                        except (ValueError, TypeError):
                            pass
                    else:
                        # Handle string values
                        setattr(settings_instance, attr_name, env_value)

    @classmethod
    def sync_from_env(cls):
        """
        Sync Settings from environment variables (.env file) for fields that haven't
        been customized in the WebUI. This is useful for initializing Settings on startup
        or when new environment variables are added.

        Only updates fields that are still at their default values (not customized via WebUI).
        """
        try:
            settings = cls.get_settings()
            if not settings or not hasattr(settings, "id"):
                # Settings doesn't exist in DB yet, get_settings will create it
                return

            # Only sync if Settings was just created (id is None means it's a new instance)
            # For existing Settings, we don't overwrite WebUI changes
            # This method is mainly for ensuring new Settings get initialized from .env
            if settings.id is None:
                cls._initialize_from_env(settings)
                if hasattr(db.session, "add"):
                    db.session.add(settings)
                db.session.commit()
        except Exception as e:
            import logging

            logger = logging.getLogger(__name__)
            logger.warning(f"Could not sync Settings from environment: {e}")
            try:
                db.session.rollback()
            except Exception:
                pass
