import re
from datetime import datetime
import argparse
import os
import MediLink_ConfigLoader
from MediLink_DataMgmt import parse_fixed_width_data, read_fixed_width_data
import MediLink_837p_encoder_library
#from tqdm import tqdm

def format_single_claim(patient_data, config, endpoint, transaction_set_control_number):
    """
    Formats a single claim into 837P segments based on the provided patient data and endpoint.
    
    Parameters:
    - patient_data: Dictionary containing detailed patient data.
    - config: Configuration settings loaded from a JSON file.
    - endpoint: The endpoint key representing the specific endpoint.
    - transaction_set_control_number: Starting transaction set control number for 837P segments.
    
    Returns:
    - String representation of the formatted 837P claim.
    """
    # Pre-resolve and enrich with Payer Name and ID for special case handling like Florida Blue.
    patient_data = MediLink_837p_encoder_library.payer_id_to_payer_name(patient_data, config, endpoint)
    
    segments = []
        
    # Initialize with standard segments for all claims
    segments.append(MediLink_837p_encoder_library.create_st_segment(transaction_set_control_number))
    segments.append(MediLink_837p_encoder_library.create_bht_segment(patient_data))

    # Submitter Name Segment and PER Contact Information (1000A Loop)
    segments.extend(MediLink_837p_encoder_library.create_1000A_submitter_name_segment(patient_data, config, endpoint))
    
    # Receiver Name Segment (1000B Loop)
    segments.extend([MediLink_837p_encoder_library.create_1000B_receiver_name_segment(config, endpoint)])
    
    # Billing Provider Segments (2010AA Loop)
    segments.extend([MediLink_837p_encoder_library.create_hl_billing_provider_segment()])
    segments.extend(MediLink_837p_encoder_library.create_nm1_billing_provider_segment(config, endpoint))

    # Pay-To Address Segment (2010AB Loop) if the Pay-To Address differs from the Billing Provider's address
    #segments.extend(MediLink_837p_encoder_library.create_nm1_payto_address_segments(config))
    
    # PRV Provider Taxonomy Segment
    #segments.extend([MediLink_837p_encoder_library.create_billing_prv_segment(config, endpoint)])
    
    # Subscriber information, possibly including endpoint-specific logic
    segments.extend(MediLink_837p_encoder_library.create_hl_subscriber_segment())
    segments.append(MediLink_837p_encoder_library.create_sbr_segment(config, patient_data, endpoint))
    segments.append(MediLink_837p_encoder_library.create_nm1_subscriber_segment(config, patient_data, endpoint))
    segments.extend(MediLink_837p_encoder_library.create_subscriber_address_segments(patient_data))
    segments.append(MediLink_837p_encoder_library.create_dmg_segment(patient_data))
    
    # Payer information (2010BB loop)
    # TODO This function now includes detailed outputs and potential user interactions with the new implementation.
    # The new implementation introduces user inputs directly in the flow, which could disrupt automated batch processes. 
    # Ensure that there are mechanisms or workflows in place to handle such interruptions appropriately.
    segments.extend([MediLink_837p_encoder_library.create_2010BB_payer_information_segment(patient_data)])
    #segments.extend(MediLink_837p_encoder_library.create_payer_address_segments(config)) OMITTED
    
    # Rendering Provider (2310B Loop)
    segments.extend(MediLink_837p_encoder_library.create_nm1_rendering_provider_segment(config))
    
    # Claim information 2300, 2310C Service Facility and 2400 loop segments
    segments.extend(MediLink_837p_encoder_library.create_clm_and_related_segments(patient_data, config))

    # Placeholder for the SE segment to be updated with actual segment count later
    segments.append("SE**{transaction_set_control_number:04d}~")

    # Update SE segment with the actual segment count and generate the final formatted 837P string
    formatted_837p = MediLink_837p_encoder_library.generate_segment_counts('\n'.join(filter(None, segments)), transaction_set_control_number)

    # Optionally, print or log the formatted 837P for debugging or verification
    # TODO (Low UI/usability) Add chart number to this.
    MediLink_ConfigLoader.log("Formatted 837P for endpoint {}.".format(endpoint), config, level="INFO")

    return formatted_837p

def write_output_file(document_segments, output_directory, endpoint_key, input_file_path, config):
    """
    Writes formatted 837P document segments to an output file with a dynamically generated name.
    
    Development Roadmap:
    - Ensure input `document_segments` is a non-empty list to avoid creating empty files.
    - Verify `output_directory` exists and is writable before proceeding. Create the directory if it does not exist.
    - Consider parameterizing the file naming convention or providing options for customization to accommodate different organizational needs.
    - Implement error handling to gracefully manage file writing failures, potentially returning a status or error message alongside the file path.
    - Incorporate logging directly within the function, accepting an optional `config` or `logger` parameter to facilitate tracking of the file writing process and outcomes.
    - Update the return value to include both the path to the output file and any relevant status information (e.g., success flag, error message) to enhance downstream error handling and user feedback.
    
    Parameters:
    - document_segments: List of strings, where each string is a segment of the 837P document to be written.
    - output_directory: String specifying the directory where the output file will be saved.
    - endpoint_key: String specifying the endpoint for which the claim is processed, used in naming the output file.
    - input_file_path: String specifying the path to the input file being processed, used in naming the output file.
    
    Returns:
    - String specifying the path to the successfully created output file. Consider returning a tuple (path, status) for enhanced error handling.
    """
    # Check if document segments are empty
    if not document_segments:
        MediLink_ConfigLoader.log("Error: Empty document segments provided. No output file created.", config, level="ERROR")
        return None
    
    # Check if the output directory exists and is writable
    if not os.path.exists(output_directory):
        try:
            os.makedirs(output_directory)
        except OSError as e:
            MediLink_ConfigLoader.log("Error: Failed to create output directory. {}".format(e), config, level="ERROR")
            return None
    elif not os.access(output_directory, os.W_OK):
        MediLink_ConfigLoader.log("Error: Output directory is not writable.", config, level="ERROR")
        return None
    
    # Generate new output file path
    base_name = os.path.splitext(os.path.basename(input_file_path))[0]
    timestamp = datetime.now().strftime("%m%d%H%M")
    new_output_file_name = "{}_{}_{}.txt".format(base_name, endpoint_key.lower(), timestamp)
    new_output_file_path = os.path.join(output_directory, new_output_file_name)

    # Write formatted 837P document to the output file
    try:
        with open(new_output_file_path, 'w') as output_file:
            output_file.write('\n'.join(document_segments))
        MediLink_ConfigLoader.log("Successfully converted and saved to {}".format(new_output_file_path), config, level="INFO")
        return new_output_file_path
    except Exception as e:
        MediLink_ConfigLoader.log("Error: Failed to write output file. {}".format(e), config, level="ERROR")
        return None

def process_file(file_path, config, endpoint_key, transaction_set_control_number):
    """
    Process the claim data from a file into the 837P format.

    Args:
        file_path (str): The path to the file containing the claim data.
        config (dict): Configuration settings loaded from a JSON file.
        endpoint_key (str): The key representing the endpoint for which the claim is being processed.
        transaction_set_control_number (int): The starting transaction set control number for 837P segments.

    Returns:
        tuple: A tuple containing the formatted claim segments and the next transaction set control number.
    """
    valid_claims, validation_errors = read_and_validate_claims(file_path, config)
    
    # Handle validation errors
    if validation_errors:
        if not MediLink_837p_encoder_library.handle_validation_errors(transaction_set_control_number, validation_errors, config):
            return None, transaction_set_control_number  # Halt processing if the user chooses

    # Process each valid claim
    formatted_claims, transaction_set_control_number = format_claims(valid_claims, config, endpoint_key, transaction_set_control_number)

    formatted_claims_str = '\n'.join(formatted_claims)  # Join formatted claims into a single string
    return formatted_claims_str, transaction_set_control_number

def read_and_validate_claims(file_path, config):
    """
    Read and validate claim data from a file.

    Args:
        file_path (str): The path to the file containing the claim data.
        config (dict): Configuration settings loaded from a JSON file.

    Returns:
        tuple: A tuple containing a list of valid parsed data and a list of validation errors.
    """
    valid_claims = []  # List to store valid parsed data
    validation_errors = []  # List to store validation errors

    # Iterate over data in the file
    for personal_info, insurance_info, service_info, service_info_2, service_info_3 in read_fixed_width_data(file_path):
        # Parse data into a usable format
        parsed_data = parse_fixed_width_data(personal_info, insurance_info, service_info, service_info_2, service_info_3, config.get('MediLink_Config', config))
        # Validate the parsed data
        is_valid, errors = validate_claim_data(parsed_data, config)
        if is_valid:
            valid_claims.append(parsed_data)  # Add valid data to the list
        else:
            validation_errors.append(errors)  # Add validation errors to the list
            # Log validation failure
            MediLink_ConfigLoader.log("Validation failed for claim data in file: {}. Errors: {}".format(file_path, errors), config, level="ERROR")

    return valid_claims, validation_errors

def format_claims(parsed_data_list, config, endpoint, starting_transaction_set_control_number):
    """
    Formats a list of parsed claim data into 837P segments.

    Parameters:
    - parsed_data_list: List of dictionaries containing parsed claim data.
    - config: Configuration settings loaded from a JSON file.
    - endpoint: The endpoint key representing the specific endpoint.
    - starting_transaction_set_control_number: Starting transaction set control number for 837P segments.

    Returns:
    - A list of formatted 837P claims and the next transaction set control number.
    """
    formatted_claims = []
    transaction_set_control_number = starting_transaction_set_control_number

    for parsed_data in parsed_data_list:
        formatted_claim = format_single_claim(parsed_data, config, endpoint, transaction_set_control_number)
        formatted_claims.append(formatted_claim)
        transaction_set_control_number += 1  # Increment for each successfully processed claim

    return formatted_claims, transaction_set_control_number

# Validation Function checks the completeness and correctness of each claim's data
def validate_claim_data(parsed_data, config, required_fields=[]):
    """
    Used by both paths. 
    
    Validates the completeness and correctness of each claim's data based on configurable requirements.

    Parameters:
    - parsed_data: Dictionary containing claim data to validate.
    - config: Configuration settings loaded from a JSON file.
    - required_fields: Optional list of tuples indicating required fields and their respective regex patterns for validation.

    Returns:
    - (bool, list): Tuple containing a boolean indicating whether the data is valid and a list of error messages if any.

    # TODO This required fields thing needs to be redone. 
    
    if required_fields is None:
        required_fields = [
            ('CHART', None),
            ('billing_provider_npi', r'^\d{10}$'),
            ('IPOLICY', None),
            ('CODEA', None),
            ('DATE', r'^\d{8}$'),
            ('AMOUNT', None),
            ('TOS', None),
            ('DIAG', None)
        ]
    """    
    errors = []
    MediLink_ConfigLoader.log("Starting claim data validation...")
    if not required_fields:
        # If no required fields are specified, assume validation is true
        return True, []
    
    expected_keys = {field[0] for field in required_fields}  # Set of expected field keys
    received_keys = set(parsed_data.keys())  # Set of keys present in the parsed data

    # Check if there is any intersection between expected keys and received keys
    if not expected_keys & received_keys:
        # Log the preview of expected and received keys
        preview_msg = "Validation skipped: No matching fields found between expected and received data."
        error_msg = "{}\nExpected keys: {}\nReceived keys: {}".format(preview_msg, expected_keys, received_keys)
        MediLink_ConfigLoader.log(error_msg, config, level="WARNING")
        print(error_msg)  # Optionally print to console for immediate feedback
        return True, [preview_msg]  # Return true to say that it's valid data anyway.

    # Check for missing or empty fields and validate patterns
    for field, pattern in required_fields:
        value = parsed_data.get(field)
        if not value:
            errors.append("Missing or empty field: {}".format(field))
        elif pattern and not re.match(pattern, value):
            errors.append("Invalid format in field {}: {}".format(field, value))

    # Validate date fields if required and ensure they are in the correct format
    date_field = 'DATE'
    date_value = parsed_data.get(date_field)
    if date_value:
        try:
            datetime.strptime(date_value, "%Y%m%d")
        except ValueError:
            errors.append("Invalid date format: {}".format(date_value))

    # Log validation errors and return
    if errors:
        for error in errors:
            MediLink_ConfigLoader.log(error, config, level="ERROR")
        return False, errors

    return True, []

def process_and_write_file(file_path, config, endpoint, starting_tscn=1):
    """        
    Process a single file, create complete 837P document with headers and trailers, and write to output file.

    Parameters:
    - file_path: Path to the .DAT file to be processed.
    - config: Configuration settings.
    - endpoint: Endpoint key.
    - starting_tscn: Starting Transaction Set Control Number.
    """
    print("Processing: {}".format(file_path))
    MediLink_ConfigLoader.log("Processing: {}".format(file_path))
    formatted_data, transaction_set_control_number = process_file(file_path, config, endpoint, starting_tscn)
    isa_header, gs_header, ge_trailer, iea_trailer = MediLink_837p_encoder_library.create_interchange_elements(config, endpoint, transaction_set_control_number - 1)
    
    # Combine everything into a single document
    complete_document = "{}\n{}\n{}\n{}\n{}".format(
        isa_header,
        gs_header,
        formatted_data,
        ge_trailer,            
        iea_trailer
    )
    
    # Write to output file
    output_file_path = write_output_file([complete_document], config.get('outputFilePath', ''), endpoint, file_path, config)
    print("File processed. Output saved to: {}".format(output_file_path))

def main():
    """
    Converts fixed-width files to 837P format for health claim submissions.

    Usage:
    ------
    1. Convert a single file:
        python MediLink_837p_encoder.py -e [endpoint] -p [file_path]

    2. Convert all files in a directory:
        python MediLink_837p_encoder.py -e [endpoint] -p [directory_path] -d

    Arguments:
    ----------
    - "-e": Specify endpoint ("AVAILITY", "OPTUMEDI", "PNT_DATA").
    - "-p": Path to file/directory for processing.
    - "-d": Flag for directory processing.

    Note: Ensure correct config file path.
    """
    parser = argparse.ArgumentParser(
        description="Converts fixed-width files to the 837P format for health claim submissions. Supports processing individual files or all files within a directory.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    parser.add_argument(
        "-e", "--endpoint", 
        required=True,
        choices=["AVAILITY", "OPTUMEDI", "PNT_DATA"],
        help="Specify the endpoint for which the conversion is intended."
    )
    parser.add_argument(
        "-p", "--path", 
        required=True,
        help="Path to the input fixed-width file or directory to process. If a directory is provided, all .DAT files within will be processed."
    )
    parser.add_argument(
        "-d", "--is-directory", 
        action='store_true',
        help="Flag indicating the path provided is a directory. If set, all .DAT files within the directory will be processed."
    )
    args = parser.parse_args()

    print("Starting the conversion process for {}. Processing {} at '{}'.".format(args.endpoint, 'directory' if args.is_directory else 'file', args.path))

    config, _ = MediLink_ConfigLoader.load_configuration()
    config = config.get('MediLink_Config', config)
    
    process_files(args.path, config, args.endpoint, args.is_directory)
    print("Conversion complete.")

def process_files(path, config, endpoint, is_directory):
    """
    Processes either a single file or all files within a directory.

    Parameters:
    - path: Path to the input fixed-width file or directory to process.
    - config: Configuration settings loaded from a JSON file.
    - endpoint: The endpoint for which the conversion is intended.
    - is_directory: Boolean flag indicating if the path is a directory.

    Returns:
    - None
    """
    if is_directory:
        MediLink_ConfigLoader.log("Processing all .DAT files in: {}".format(path))
        for file_name in os.listdir(path):
            if file_name.endswith(".DAT"):
                file_path = os.path.join(path, file_name)
                process_and_write_file(file_path, config, endpoint)
    else:
        MediLink_ConfigLoader.log("Processing the single file: {}".format(path))
        process_and_write_file(path, config, endpoint)

if __name__ == "__main__":
    main()
    
# The functions below are the ones that are used as non-main library by outside scripts.
#######################################################################################

def convert_files_for_submission(detailed_patient_data, config):
    """    
    Processes detailed patient data for submission based on their confirmed endpoints,
    generating a separate 837P file for each endpoint.

    Parameters:
    - detailed_patient_data: A list containing detailed patient data with endpoint information.
    - config: Configuration settings loaded from a JSON file.

    Returns:
    - A list of paths to the converted files ready for submission.
    """

    # Initialize a dictionary to hold patient data segregated by confirmed endpoints
    data_by_endpoint = {}
        
    # Populate the dictionary with patient data
    for data in detailed_patient_data:
        endpoint = data['confirmed_endpoint']
        if endpoint not in data_by_endpoint:
            data_by_endpoint[endpoint] = []
        data_by_endpoint[endpoint].append(data)

    # List to store paths of converted files for each endpoint
    converted_files_paths = []

    # Determine the total number of unique endpoints for progress bar
    # total_endpoints = len(data_by_endpoint)

    # Iterate over each endpoint and process its corresponding patient data
    for endpoint, patient_data_list in data_by_endpoint.items():
        # tqdm(data_by_endpoint.items(), desc="Creating 837p(s)", total=total_endpoints, ascii=True)
        # Each endpoint might have multiple patients' data to be processed into a single 837P file
        if patient_data_list:
            converted_path = process_claim(config['MediLink_Config'], endpoint, patient_data_list)
            if converted_path:
                converted_files_paths.append(converted_path)
        
    return converted_files_paths

def process_claim(config, endpoint, patient_data_list):
    """
    Processes patient data for a specified endpoint, converting it into the 837P format.
    Generates a separate 837P file for each endpoint based on detailed patient data.

    Parameters:
    - config: Configuration settings loaded from a JSON file.
    - endpoint_key: The key representing the endpoint for which the data is being processed.
    - patient_data_list: A list of dictionaries, each containing detailed patient data.

    Returns:
    - Path to the converted file, or None if an error occurs.
    
    TODO (LOW) Why are there duplicated interchange flows? Because the arg if we're doing a .dat directory or not. 
    Although, that shouldn't be making duplicates of these interchange headers. That's still confusing and could end up making 
    duplicate interchange headers because processing .dat in batch might be fast enough to be a problem.
    """
    output_directory = MediLink_837p_encoder_library.get_output_directory(config)
    if not output_directory:
        return None

    # Initialize the transaction set control number and document segments
    transaction_set_control_number = 1
    document_segments = []

    # Process each patient's data in the list
    for patient_data in patient_data_list:
        # Validate each patient's data
        is_valid, validation_errors = validate_claim_data(patient_data, config)
        if is_valid:
            # Format the claim if data is valid
            formatted_claim = format_single_claim(patient_data, config, endpoint, transaction_set_control_number)
            document_segments.append(formatted_claim)
            transaction_set_control_number += 1
        else:
            if MediLink_837p_encoder_library.handle_validation_errors(transaction_set_control_number, validation_errors, config):
                continue # Skip the current patient

    # Create interchange elements with the final transaction set control number
    isa_header, gs_header, ge_trailer, iea_trailer = MediLink_837p_encoder_library.create_interchange_elements(config, endpoint, transaction_set_control_number - 1)

    # Insert headers at the beginning and append trailers at the end of document segments
    document_segments.insert(0, gs_header)
    document_segments.insert(0, isa_header)
    document_segments.extend([ge_trailer, iea_trailer])

    # Write the complete 837P document to an output file and return its path
    return write_output_file(document_segments, output_directory, endpoint, patient_data_list[0]['file_path'], config)