from typing import List, Tuple
import os
import sys


def read_text_file(path: str) -> List[str]:
    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.readlines()
    except FileNotFoundError:
        raise FileNotFoundError(f"File not found: {path}")
    except PermissionError:
        raise PermissionError(f"Permission denied while reading: {path}")
    except OSError as e:
        raise OSError(f"Error reading file '{path}': {e}")


def write_text_file(path: str, lines: List[str]) -> None:
    try:
        with open(path, "w", encoding="utf-8") as f:
            f.writelines(lines)
    except PermissionError:
        raise PermissionError(f"Permission denied while writing: {path}")
    except OSError as e:
        raise OSError(f"Error writing file '{path}': {e}")


def leading_whitespace(s: str) -> str:
    i = 0
    while i < len(s) and s[i] in (" ", "\t"):
        i += 1
    return s[:i]


def has_newline(s: str) -> bool:
    return s.endswith("\n")


def is_comment_or_empty(line: str) -> bool:
    stripped = line.strip()
    return stripped == "" or stripped.startswith("#") or stripped.startswith(";")


def convert_proto_udp_to_tcp(lines: List[str]) -> Tuple[List[str], bool]:
    changed = False
    out = []
    for line in lines:
        if not is_comment_or_empty(line):
            stripped = line.strip()
            if stripped.startswith("proto"):
                parts = stripped.split()
                if len(parts) >= 2 and parts[1].startswith("udp"):
                    parts[1] = "tcp"
                    prefix = leading_whitespace(line)
                    newline = "\n" if has_newline(line) else ""
                    line = f"{prefix}{' '.join(parts)}{newline}"
                    changed = True
        out.append(line)
    return out, changed


def update_remote_port_to_443(lines: List[str]) -> Tuple[List[str], int]:
    count = 0
    out = []
    for line in lines:
        if not is_comment_or_empty(line):
            stripped = line.strip()
            if stripped.startswith("remote "):
                parts = stripped.split()
                if len(parts) >= 3 and parts[2].isdigit():
                    parts[2] = "443"
                    prefix = leading_whitespace(line)
                    newline = "\n" if has_newline(line) else ""
                    line = f"{prefix}{' '.join(parts)}{newline}"
                    count += 1
        out.append(line)
    return out, count


def transform_tls_auth_to_tls_crypt_and_disable_key_direction(lines: List[str]) -> Tuple[List[str], bool, int]:
    tag_changed = False
    kd_count = 0
    out = []
    for line in lines:
        new_line = line
        if not is_comment_or_empty(line):
            stripped = line.strip()
            if stripped == "<tls-auth>":
                prefix = leading_whitespace(line)
                newline = "\n" if has_newline(line) else ""
                new_line = f"{prefix}<tls-crypt>{newline}"
                tag_changed = True
            elif stripped == "</tls-auth>":
                prefix = leading_whitespace(line)
                newline = "\n" if has_newline(line) else ""
                new_line = f"{prefix}</tls-crypt>{newline}"
                tag_changed = True
            elif stripped.startswith("key-direction"):
                prefix = leading_whitespace(line)
                newline = "\n" if has_newline(line) else ""
                new_line = f"{prefix}# {stripped}{newline}"
                kd_count += 1
        out.append(new_line)
    return out, tag_changed, kd_count


def comment_explicit_exit_notify(lines: List[str]) -> Tuple[List[str], int]:
    count = 0
    out = []
    for line in lines:
        new_line = line
        if not is_comment_or_empty(line):
            stripped = line.strip()
            if stripped.startswith("explicit-exit-notify"):
                prefix = leading_whitespace(line)
                newline = "\n" if has_newline(line) else ""
                new_line = f"{prefix}# {stripped}{newline}"
                count += 1
        out.append(new_line)
    return out, count


def build_output_path(input_path: str) -> str:
    base_dir = os.path.dirname(input_path)
    base_name = os.path.basename(input_path)
    name, ext = os.path.splitext(base_name)
    return os.path.join(base_dir, f"{name}_hardened{ext}")


def validate_input_path(path: str) -> None:
    if not os.path.isfile(path):
        raise FileNotFoundError(f"Input file does not exist: {path}")
    _, ext = os.path.splitext(path)
    if ext.lower() != ".ovpn":
        raise ValueError(f"Input file must have .ovpn extension, got: {ext}")


def harden_config(input_path: str, output_path: str) -> None:
    lines = read_text_file(input_path)
    lines, proto_changed = convert_proto_udp_to_tcp(lines)
    lines, remote_changed = update_remote_port_to_443(lines)
    lines, tls_tag_changed, kd_count = transform_tls_auth_to_tls_crypt_and_disable_key_direction(lines)
    lines, exit_notify_count = comment_explicit_exit_notify(lines)
    write_text_file(output_path, lines)
    print("Hardening completed:")
    print(f"- proto udp -> tcp: {'changed' if proto_changed else 'no change'}")
    print(f"- remote port -> 443: {remote_changed} line(s) updated")
    print(f"- <tls-auth> -> <tls-crypt>: {'changed' if tls_tag_changed else 'no change'}")
    print(f"- key-direction commented: {kd_count} line(s)")
    print(f"- explicit-exit-notify commented: {exit_notify_count} line(s)")
    print(f"- Output written to: {output_path}")
