"""BCP (Bulk Copy Program) data source implementation."""

from __future__ import annotations

import os
import subprocess
import threading

from typing import TYPE_CHECKING

from dependency_injector.wiring import Provide, inject


if TYPE_CHECKING:
    from data_exchange_agent.config.manager import ConfigManager
    from data_exchange_agent.config.sections.connections.odbc.sqlserver import (
        SQLServerConnectionConfig,
    )

from data_exchange_agent.constants import config_keys, container_keys
from data_exchange_agent.constants.paths import build_actual_results_folder_path
from data_exchange_agent.data_sources.base import BaseDataSource
from data_exchange_agent.utils.sf_logger import SFLogger


# BCP defaults - always use these values
DEFAULT_FIELD_TERMINATOR = "\x1f"
DEFAULT_ROW_TERMINATOR = "\\n"
DEFAULT_ENCODING = "UTF8"
DEFAULT_NETWORK_PACKET_SIZE_IN_BYTES = 32768  # 32KB


class BCPDataSource(BaseDataSource):
    """
    A BCP (Bulk Copy Program) data source implementation.

    This class provides a way to export data from a SQL Server database using
    the BCP command-line utility. The data is exported to CSV or other formats
    that BCP supports.

    Attributes:
        statement (str): The SQL statement to execute
        results_folder_path (str): The path to the results folder
        base_file_name (str): The base file name for output files

    """

    @property
    def statement(self) -> str:
        """The statement to execute."""
        return self._statement

    @property
    def results_folder_path(self) -> str:
        """The path to the results folder."""
        return self._results_folder_path

    @property
    def base_file_name(self) -> str:
        """The base file name."""
        return self._base_file_name

    @inject
    def __init__(
        self,
        engine: str,
        statement: str,
        results_folder_path: str | None = None,
        base_file_name: str = "result",
        suggested_batch_size: int | None = None,
        database: str | None = None,
        use_database_command: str | None = None,
        logger: SFLogger = Provide[container_keys.SF_LOGGER],
        program_config: ConfigManager = Provide[container_keys.PROGRAM_CONFIG],
    ) -> None:
        """
        Initialize a new BCPDataSource.

        Args:
            engine (str): The database engine type
            statement (str): The SQL statement to execute
            results_folder_path (str): The path to the results folder. If None, uses the default actual results
                folder path
            base_file_name (str): The base file name for output files
            suggested_batch_size (int | None): Not used by BCP, accepted for API compatibility with other data sources
            database (str | None): Optional database name to use for the BCP -d flag.
                When provided, overrides the database from the connection configuration.
            use_database_command (str | None): Not used by BCP (database switching is handled
                via the -d flag). Accepted for API compatibility with other data sources.
            logger (SFLogger): The logger instance
            program_config (ConfigManager): The program configuration manager

        """
        self.logger: SFLogger = logger
        self._statement: str = statement
        self._results_folder_path: str = build_actual_results_folder_path() if results_folder_path is None else results_folder_path

        source_connections_config: dict[str, SQLServerConnectionConfig] = program_config[config_keys.CONNECTIONS__SOURCE]
        engine_config: SQLServerConnectionConfig = source_connections_config[engine]

        self._base_file_name: str = base_file_name
        self._server: str = engine_config.host
        self._database: str = database if database else engine_config.database
        self._username: str = engine_config.username
        self._password: str = engine_config.password
        self._trust_server_certificate: bool = bool(engine_config.extra_options.get("trustServerCertificate", False))

        # Authentication is determined solely by the connection configuration
        self._use_windows_auth: bool = engine_config.use_windows_auth

        # BCP always uses default delimiters and UTF-8 encoding
        self._field_terminator: str = DEFAULT_FIELD_TERMINATOR
        self._row_terminator: str = DEFAULT_ROW_TERMINATOR
        self._encoding: str = DEFAULT_ENCODING

        # Get encrypt setting from connection config
        self._encrypt: bool | None = engine_config.encrypt_bool

    def export_data(self) -> bool:
        """
        Export data using BCP command.

        Returns:
            bool: True if the data was exported successfully, False otherwise

        Raises:
            Exception: If the BCP command fails

        """
        try:
            # Create output directory
            os.makedirs(self.results_folder_path, exist_ok=True)

            # Build output file path
            output_file = os.path.join(self.results_folder_path, f"{self.base_file_name}_001.csv")

            # Build BCP command
            bcp_command = self._build_bcp_command(output_file)

            # Log the command (mask password)
            masked_command = self._mask_password_in_command(bcp_command)
            self.logger.info(f"Executing BCP command: {masked_command}")

            # Check if BCP is installed
            try:
                bcp_check = subprocess.run(
                    ["bcp", "-v"],
                    capture_output=True,
                    text=True,
                    check=False,
                )
                if bcp_check.returncode != 0:
                    raise Exception("BCP utility is not installed or not available in PATH.")
            except Exception as e:
                self.logger.error(f"Error checking if BCP is installed. Error: {e}")
                raise Exception("BCP utility is not installed or not available in PATH.") from e

            # Execute BCP command with real-time output streaming
            process = subprocess.Popen(
                bcp_command,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                bufsize=1,  # Line buffered
            )

            stdout_lines = []
            stderr_lines = []

            # Function to read stdout stream
            def read_stdout():
                if process.stdout is None:
                    return
                for line in process.stdout:
                    line = line.rstrip()
                    if line:  # Only log non-empty lines
                        self.logger.info(f"BCP: {line}")
                        stdout_lines.append(line)

            # Function to read stderr stream
            def read_stderr():
                if process.stderr is None:
                    return
                for line in process.stderr:
                    line = line.rstrip()
                    if line:  # Only log non-empty lines
                        self.logger.error(f"BCP stderr: {line}")
                        stderr_lines.append(line)

            # Start threads to read both streams concurrently
            stdout_thread = threading.Thread(target=read_stdout)
            stderr_thread = threading.Thread(target=read_stderr)

            stdout_thread.start()
            stderr_thread.start()

            # Wait for both threads to complete
            stdout_thread.join()
            stderr_thread.join()

            # Wait for process to complete
            return_code = process.wait()

            if return_code != 0:
                stdout_text = "\n".join(stdout_lines)
                stderr_text = "\n".join(stderr_lines)
                error_message = f"""BCP command failed with return code {return_code}.
                Stdout: {stdout_text}
                Stderr: {stderr_text}"""
                self.logger.error(error_message)
                raise Exception(error_message)

            self.logger.info("BCP export completed successfully.")
            return True

        except Exception as e:
            self.logger.error(f"Error exporting data using BCP. Error: {e}")
            raise e

    def _build_bcp_command(self, output_file: str) -> list[str]:
        """
        Build the BCP command as a list of arguments.

        Args:
            output_file (str): The path to the output file

        Returns:
            list[str]: The BCP command as a list of arguments

        """
        # BCP can accept a query or a table name
        # If statement contains spaces or SELECT, treat as query
        query_or_table = self._statement.strip()

        # Determine if it's a query or table
        is_query = any(keyword in query_or_table.upper() for keyword in ["SELECT", "EXEC", "EXECUTE", "WITH"])

        # Build base command
        cmd_parts = ["bcp"]

        if is_query:
            # For queries, add as-is and use queryout
            cmd_parts.append(query_or_table)
            cmd_parts.append("queryout")
        else:
            # For tables, use regular format
            cmd_parts.append(query_or_table)
            cmd_parts.append("out")

        cmd_parts.append(output_file)

        # Add authentication based on connection configuration
        if self._use_windows_auth:
            cmd_parts.append("-T")
        else:
            cmd_parts.extend(
                [
                    "-U",
                    self._username,
                    "-P",
                    self._password,
                ]
            )

        # Build server string with connection properties
        server_string = self._build_server_string()
        cmd_parts.extend(
            [
                "-S",
                server_string,
                "-d",
                self._database,
            ]
        )

        # Add format options
        cmd_parts.extend(
            [
                "-c",  # Character data type
                "-t",
                self._field_terminator,
                "-r",
                self._row_terminator,
            ]
        )

        # Add encoding if specified
        if self._encoding:
            cmd_parts.extend(["-C", self._encoding])

        # Set network packet size to maximum (32KB) for better performance
        cmd_parts.extend(["-a", str(DEFAULT_NETWORK_PACKET_SIZE_IN_BYTES)])

        return cmd_parts

    def _build_server_string(self) -> str:
        """
        Build the server connection string with optional connection properties.

        Returns:
            str: Server string with connection properties if needed

        """
        # Add connection properties if specified
        if self._trust_server_certificate or self._encrypt is not None:
            properties = []

            if self._encrypt is not None:
                encrypt_value = "yes" if self._encrypt else "no"
                properties.append(f"Encrypt={encrypt_value}")

            if self._trust_server_certificate:
                properties.append("TrustServerCertificate=yes")

            if properties:
                # Join server and properties with semicolon
                connection_string = f"{self._server};{';'.join(properties)}"
                return connection_string

        return self._server

    def _mask_password_in_command(self, command: list[str]) -> str:
        """
        Mask password in command list for logging.

        Args:
            command (list[str]): The command as a list of arguments

        Returns:
            str: The command string with password masked

        """
        if not self._use_windows_auth and self._password:
            # Create a copy of the command list to avoid modifying the original
            masked_command = command.copy()
            # Find the password argument (follows -P flag)
            try:
                password_index = masked_command.index("-P")
                if password_index + 1 < len(masked_command):
                    masked_command[password_index + 1] = "***"
            except ValueError:
                pass  # -P flag not found
            return " ".join(masked_command)
        return " ".join(command)
