  # #region llm_provider [C:3] [TYPE Module] [SEMANTICS sqlalchemy, llm, provider, encryption, config]
  # @defgroup Services Module group.
  # @BRIEF Service for managing LLM provider configurations with encrypted API keys.
  # @LAYER Domain
  # @RELATION DEPENDS_ON -> [LLMProvider]
  # @RELATION DEPENDS_ON -> [EncryptionManager]
  # @RELATION DEPENDS_ON -> [LLMProviderConfig]
  # @RATIONALE EncryptionManager imported from core/encryption.py (extracted from this module)
  #            to decouple core encryption from LLM domain — see [SEC:C-1].
> from typing import TYPE_CHECKING
  
> from cryptography.exceptions import InvalidTag
> from sqlalchemy.orm import Session
  
> from ..core.encryption import EncryptionManager
> from ..core.logger import belief_scope, logger
> from ..models.llm import LLMProvider
  
- if TYPE_CHECKING:
-     from ..plugins.llm_analysis.models import LLMProviderConfig
  
> MASKED_API_KEY_PLACEHOLDER = "********"
  
  
  # #region mask_api_key [C:2] [TYPE Function]
  # @ingroup Services
  # @BRIEF Mask an API key for safe display, showing first 4 and last 4 characters.
  # @PRE  api_key is a plaintext string or None.
  # @POST  Returns "****" for very short keys; "{first 2}...{last 2}" for <=8 chars;
  #        "{first 4}...{last 4}" for longer keys; "" for None/empty.
> def mask_api_key(api_key: str | None) -> str:
>     if not api_key:
>         return ""
>     if len(api_key) <= 4:
>         return "****"
>     if len(api_key) <= 8:
>         return f"{api_key[:2]}...{api_key[-2:]}"
>     return f"{api_key[:4]}...{api_key[-4:]}"
  
  
  # #endregion mask_api_key
  
  
  # #region is_masked_or_placeholder [C:2] [TYPE Function]
  # @ingroup Services
  # @BRIEF Predicate: True when api_key is None, empty, "********", or contains "...".
  # @PRE  api_key can be None.
  # @POST  Returns True only for non-real-key values.
> def is_masked_or_placeholder(api_key: str | None) -> bool:
>     if not api_key:
>         return True
>     return api_key == MASKED_API_KEY_PLACEHOLDER or "..." in api_key
  
  
  # #endregion is_masked_or_placeholder
  
  
  # NOTE: EncryptionManager is now imported from ..core.encryption
  
  
  # #region LLMProviderService [C:3] [TYPE Class]
  # @defgroup Services Module group.
  # @BRIEF Service to manage LLM provider lifecycle.
  # @RELATION DEPENDS_ON -> [LLMProvider]
  # @RELATION DEPENDS_ON -> [EncryptionManager]
  # @RELATION DEPENDS_ON -> [LLMProviderConfig]
> class LLMProviderService:
      # region LLMProviderService_init [TYPE Function]
      # @PURPOSE: Initialize the service with database session.
      # @PRE  db must be a valid SQLAlchemy Session.
      # @POST  Service ready for provider operations.
      # @RELATION DEPENDS_ON ->[EncryptionManager]
>     def __init__(self, db: Session):
>         self.db = db
>         self.encryption = EncryptionManager()
  
      # endregion LLMProviderService_init
  
      # region get_all_providers [TYPE Function]
      # @PURPOSE: Returns all configured LLM providers.
      # @PRE  Database connection must be active.
      # @POST  Returns list of all LLMProvider records.
      # @RELATION DEPENDS_ON ->[LLMProvider]
>     def get_all_providers(self) -> list[LLMProvider]:
>         with belief_scope("get_all_providers"):
>             return self.db.query(LLMProvider).all()
  
      # endregion get_all_providers
  
      # region get_provider [TYPE Function]
      # @PURPOSE: Returns a single LLM provider by ID.
      # @PRE  provider_id must be a valid string.
      # @POST  Returns LLMProvider or None if not found.
      # @RELATION DEPENDS_ON ->[LLMProvider]
>     def get_provider(self, provider_id: str) -> LLMProvider | None:
>         with belief_scope("get_provider"):
>             return (
>                 self.db.query(LLMProvider).filter(LLMProvider.id == provider_id).first()
>             )
  
      # endregion get_provider
  
      # region create_provider [TYPE Function]
      # @PURPOSE: Creates a new LLM provider with encrypted API key.
      # @PRE  config must contain valid provider configuration.
      # @POST  New provider created and persisted to database.
      # @RELATION DEPENDS_ON ->[LLMProviderConfig]
      # @RELATION DEPENDS_ON ->[LLMProvider]
      # @RELATION CALLS ->[EXT:method:encrypt]
>     def create_provider(self, config: "LLMProviderConfig") -> LLMProvider:
>         with belief_scope("create_provider"):
>             encrypted_key = self.encryption.encrypt(config.api_key)
>             db_provider = LLMProvider(
>                 provider_type=config.provider_type.value,
>                 name=config.name,
>                 base_url=config.base_url,
>                 api_key=encrypted_key,
>                 default_model=config.default_model,
>                 is_active=config.is_active,
>                 is_multimodal=config.is_multimodal,
>                 max_images=config.max_images,
>                 context_window=config.context_window,
>                 max_output_tokens=config.max_output_tokens,
>             )
>             self.db.add(db_provider)
>             self.db.commit()
>             self.db.refresh(db_provider)
>             return db_provider
  
      # endregion create_provider
  
      # region update_provider [TYPE Function]
      # @PURPOSE: Updates an existing LLM provider.
      # @PRE  provider_id must exist, config must be valid.
      # @POST  Provider updated and persisted to database.
      # @RELATION DEPENDS_ON ->[LLMProviderConfig]
      # @RELATION DEPENDS_ON ->[LLMProvider]
      # @RELATION CALLS ->[EXT:method:encrypt]
>     def update_provider(
>         self, provider_id: str, config: "LLMProviderConfig"
>     ) -> LLMProvider | None:
>         with belief_scope("update_provider"):
>             db_provider = self.get_provider(provider_id)
>             if not db_provider:
!                 return None
  
>             db_provider.provider_type = config.provider_type.value
>             db_provider.name = config.name
>             db_provider.base_url = config.base_url
              # Ignore masked/placeholder values; they are display-only and must not overwrite secrets.
>             if not is_masked_or_placeholder(config.api_key):
>                 db_provider.api_key = self.encryption.encrypt(config.api_key)
>             db_provider.default_model = config.default_model
>             db_provider.is_active = config.is_active
>             db_provider.is_multimodal = config.is_multimodal
>             db_provider.max_images = config.max_images
>             db_provider.context_window = config.context_window
>             db_provider.max_output_tokens = config.max_output_tokens
  
>             self.db.commit()
>             self.db.refresh(db_provider)
>             return db_provider
  
      # endregion update_provider
  
      # region set_max_images [TYPE Function]
      # @PURPOSE: Updates only the max_images field for a provider.
      # @PRE  provider_id must exist.
      # @POST  Returns updated provider or None if not found.
      # @RELATION DEPENDS_ON ->[LLMProvider]
>     def set_max_images(self, provider_id: str, max_images: int | None) -> LLMProvider | None:
>         with belief_scope("set_max_images"):
>             db_provider = self.get_provider(provider_id)
>             if not db_provider:
>                 return None
>             db_provider.max_images = max_images
>             self.db.commit()
>             self.db.refresh(db_provider)
>             return db_provider
  
      # endregion set_max_images
  
      # region delete_provider [TYPE Function]
      # @PURPOSE: Deletes an LLM provider.
      # @PRE  provider_id must exist.
      # @POST  Provider removed from database.
      # @RELATION DEPENDS_ON ->[LLMProvider]
>     def delete_provider(self, provider_id: str) -> bool:
>         with belief_scope("delete_provider"):
>             db_provider = self.get_provider(provider_id)
>             if not db_provider:
>                 return False
>             self.db.delete(db_provider)
>             self.db.commit()
>             return True
  
      # endregion delete_provider
  
      # region get_decrypted_api_key [TYPE Function]
      # @PURPOSE: Returns the decrypted API key for a provider.
      # @PRE  provider_id must exist with valid encrypted key.
      # @POST  Returns decrypted API key or None on failure.
      # @RELATION DEPENDS_ON ->[LLMProvider]
      # @RELATION CALLS ->[EXT:method:decrypt]
>     def get_decrypted_api_key(self, provider_id: str) -> str | None:
>         with belief_scope("get_decrypted_api_key"):
>             db_provider = self.get_provider(provider_id)
>             if not db_provider:
>                 logger.warning(
>                     f"[get_decrypted_api_key] Provider {provider_id} not found in database"
>                 )
>                 return None
  
>             logger.info(f"[get_decrypted_api_key] Provider found: {db_provider.id}")
>             logger.info(
>                 f"[get_decrypted_api_key] Encrypted API key length: {len(db_provider.api_key) if db_provider.api_key else 0}"
>             )
  
>             try:
>                 decrypted_key = self.encryption.decrypt(db_provider.api_key)
>                 logger.info(
>                     f"[get_decrypted_api_key] Decryption successful, key length: {len(decrypted_key) if decrypted_key else 0}"
>                 )
>                 return decrypted_key
>             except InvalidTag as e:
!                 logger.error(
!                     f"[get_decrypted_api_key] Integrity check failed (InvalidTag): {e!s}. "
!                     "The encrypted API key may be corrupted or the ENCRYPTION_KEY has changed."
!                 )
!                 return None
>             except ValueError as e:
!                 logger.error(
!                     f"[get_decrypted_api_key] Decryption format error (ValueError): {e!s}. "
!                     "The encrypted data may not be valid Fernet ciphertext."
!                 )
!                 return None
>             except Exception as e:
>                 logger.error(
>                     f"[get_decrypted_api_key] Decryption failed with unexpected error "
>                     f"({type(e).__name__}): {e!s}"
>                 )
>                 return None
  
      # endregion get_decrypted_api_key
  
      # region get_provider_token_config [TYPE Function]
      # @PURPOSE: Returns provider token limits for batch sizing.
      # @PRE  provider_id must be valid.
      # @POST  Returns dict with model name, context_window, max_output_tokens.
      #        Values from DB take priority; None means "use PROVIDER_DEFAULTS fallback".
      # @RATIONALE Centralised helper — both _batch_proc.py and _batch_sizer.py need
      #            the same resolution logic. Avoids duplicating DB queries and defaults.
>     def get_provider_token_config(self, provider_id: str) -> dict:
!         provider = self.get_provider(provider_id)
!         if not provider:
!             return {"model": None, "context_window": None, "max_output_tokens": None}
!         return {
!             "model": provider.default_model or "gpt-4o-mini",
!             "context_window": provider.context_window,
!             "max_output_tokens": provider.max_output_tokens,
!         }
      # endregion get_provider_token_config
  
  
  # #endregion LLMProviderService
  
  # #endregion llm_provider
