#!/usr/bin/env python3
"""
Audit Schedule Optimizer - Risk-Based Internal Audit Planning

Generates optimized audit schedules based on process risk levels,
previous findings, and resource constraints.

Usage:
    python audit_schedule_optimizer.py --processes processes.json
    python audit_schedule_optimizer.py --interactive
    python audit_schedule_optimizer.py --processes processes.json --output json
"""

import argparse
import json
import sys
from dataclasses import dataclass, field, asdict
from datetime import datetime, timedelta
from typing import List, Dict, Optional
from enum import Enum


class RiskLevel(Enum):
    HIGH = "High"
    MEDIUM = "Medium"
    LOW = "Low"


class AuditFrequency(Enum):
    QUARTERLY = 90
    SEMI_ANNUAL = 180
    ANNUAL = 365
    EXTENDED = 540  # 18 months


@dataclass
class Process:
    name: str
    iso_clause: str
    risk_level: RiskLevel
    last_audit_date: Optional[str] = None
    previous_findings: int = 0
    criticality_score: int = 5  # 1-10 scale
    notes: str = ""


@dataclass
class AuditSlot:
    process_name: str
    iso_clause: str
    scheduled_date: str
    risk_level: str
    priority_score: float
    days_overdue: int = 0
    rationale: str = ""


@dataclass
class AuditSchedule:
    generated_date: str
    schedule_period: str
    total_audits: int
    audits_by_quarter: Dict[str, int]
    schedule: List[Dict]
    recommendations: List[str]


class AuditScheduleOptimizer:
    """Optimizer for risk-based audit scheduling."""

    # Frequency mapping by risk level
    FREQUENCY_MAP = {
        RiskLevel.HIGH: AuditFrequency.QUARTERLY,
        RiskLevel.MEDIUM: AuditFrequency.SEMI_ANNUAL,
        RiskLevel.LOW: AuditFrequency.ANNUAL,
    }

    # ISO 13485 required processes
    REQUIRED_PROCESSES = [
        ("Document Control", "4.2"),
        ("Management Review", "5.6"),
        ("Training and Competency", "6.2"),
        ("Design Control", "7.3"),
        ("Purchasing", "7.4"),
        ("Production Control", "7.5"),
        ("Equipment Calibration", "7.6"),
        ("Customer Feedback", "8.2.1"),
        ("Internal Audit", "8.2.2"),
        ("Nonconforming Product", "8.3"),
        ("CAPA", "8.5"),
    ]

    def __init__(self, processes: List[Process], audit_days_per_month: int = 4):
        self.processes = processes
        self.audit_days_per_month = audit_days_per_month
        self.today = datetime.now()

    def calculate_priority_score(self, process: Process) -> float:
        """Calculate audit priority score based on multiple factors."""
        score = 0.0

        # Base risk score (40% weight)
        risk_scores = {RiskLevel.HIGH: 10, RiskLevel.MEDIUM: 6, RiskLevel.LOW: 3}
        score += risk_scores[process.risk_level] * 0.4

        # Overdue factor (30% weight)
        if process.last_audit_date:
            last_audit = datetime.strptime(process.last_audit_date, "%Y-%m-%d")
            days_since = (self.today - last_audit).days
            required_frequency = self.FREQUENCY_MAP[process.risk_level].value
            overdue_ratio = days_since / required_frequency
            score += min(overdue_ratio * 10, 10) * 0.3
        else:
            # Never audited = highest priority
            score += 10 * 0.3

        # Previous findings factor (20% weight)
        findings_score = min(process.previous_findings * 2, 10)
        score += findings_score * 0.2

        # Criticality factor (10% weight)
        score += process.criticality_score * 0.1

        return round(score, 2)

    def get_days_overdue(self, process: Process) -> int:
        """Calculate days overdue for audit."""
        if not process.last_audit_date:
            return 365  # Assume 1 year overdue if never audited

        last_audit = datetime.strptime(process.last_audit_date, "%Y-%m-%d")
        required_frequency = self.FREQUENCY_MAP[process.risk_level].value
        next_due = last_audit + timedelta(days=required_frequency)
        days_overdue = (self.today - next_due).days

        return max(0, days_overdue)

    def generate_schedule(self, months_ahead: int = 12) -> AuditSchedule:
        """Generate optimized audit schedule."""
        # Calculate priority scores
        prioritized = []
        for process in self.processes:
            priority = self.calculate_priority_score(process)
            overdue = self.get_days_overdue(process)
            prioritized.append((process, priority, overdue))

        # Sort by priority (descending)
        prioritized.sort(key=lambda x: x[1], reverse=True)

        # Generate schedule slots
        schedule = []
        current_date = self.today
        audits_per_quarter = {"Q1": 0, "Q2": 0, "Q3": 0, "Q4": 0}

        for process, priority, overdue in prioritized:
            # Determine schedule date based on priority
            if overdue > 0:
                # Overdue: schedule within next 30 days
                scheduled_date = current_date + timedelta(days=min(30, overdue // 10 + 7))
            elif priority > 7:
                # High priority: within 60 days
                scheduled_date = current_date + timedelta(days=30)
            elif priority > 4:
                # Medium priority: within 120 days
                scheduled_date = current_date + timedelta(days=90)
            else:
                # Low priority: within 180 days
                scheduled_date = current_date + timedelta(days=180)

            # Cap at months_ahead
            max_date = current_date + timedelta(days=months_ahead * 30)
            if scheduled_date > max_date:
                scheduled_date = max_date

            # Track quarter distribution
            quarter = f"Q{(scheduled_date.month - 1) // 3 + 1}"
            audits_per_quarter[quarter] += 1

            # Generate rationale
            rationale_parts = []
            if overdue > 0:
                rationale_parts.append(f"{overdue} days overdue")
            if process.previous_findings > 0:
                rationale_parts.append(f"{process.previous_findings} previous findings")
            if process.risk_level == RiskLevel.HIGH:
                rationale_parts.append("high-risk process")
            rationale = "; ".join(rationale_parts) if rationale_parts else "Scheduled per frequency"

            slot = AuditSlot(
                process_name=process.name,
                iso_clause=process.iso_clause,
                scheduled_date=scheduled_date.strftime("%Y-%m-%d"),
                risk_level=process.risk_level.value,
                priority_score=priority,
                days_overdue=overdue,
                rationale=rationale
            )
            schedule.append(slot)

        # Generate recommendations
        recommendations = self._generate_recommendations(prioritized)

        return AuditSchedule(
            generated_date=self.today.strftime("%Y-%m-%d"),
            schedule_period=f"{self.today.strftime('%Y-%m-%d')} to {(self.today + timedelta(days=months_ahead * 30)).strftime('%Y-%m-%d')}",
            total_audits=len(schedule),
            audits_by_quarter=audits_per_quarter,
            schedule=[asdict(s) for s in schedule],
            recommendations=recommendations
        )

    def _generate_recommendations(self, prioritized: List) -> List[str]:
        """Generate recommendations based on analysis."""
        recommendations = []

        # Check for overdue audits
        overdue_count = sum(1 for _, _, overdue in prioritized if overdue > 0)
        if overdue_count > 0:
            recommendations.append(
                f"URGENT: {overdue_count} process(es) overdue for audit. "
                "Prioritize these to maintain compliance."
            )

        # Check for high-risk processes
        high_risk_count = sum(1 for p, _, _ in prioritized if p.risk_level == RiskLevel.HIGH)
        if high_risk_count > 3:
            recommendations.append(
                f"High audit burden: {high_risk_count} high-risk processes. "
                "Consider quarterly resource allocation."
            )

        # Check for processes with multiple findings
        finding_processes = [(p.name, p.previous_findings) for p, _, _ in prioritized if p.previous_findings >= 3]
        if finding_processes:
            names = ", ".join([name for name, _ in finding_processes[:3]])
            recommendations.append(
                f"Recurring issues in: {names}. "
                "Consider focused audits or process improvement initiatives."
            )

        # Check for never-audited processes
        never_audited = [p.name for p, _, _ in prioritized if not p.last_audit_date]
        if never_audited:
            recommendations.append(
                f"Never audited: {', '.join(never_audited[:3])}. "
                "Include in next audit cycle."
            )

        if not recommendations:
            recommendations.append("Audit program is on track. Maintain scheduled frequency.")

        return recommendations


def format_text_output(schedule: AuditSchedule) -> str:
    """Format schedule as text report."""
    lines = [
        "=" * 70,
        "AUDIT SCHEDULE OPTIMIZATION REPORT",
        "=" * 70,
        f"Generated: {schedule.generated_date}",
        f"Period: {schedule.schedule_period}",
        f"Total Audits: {schedule.total_audits}",
        "",
        "Quarterly Distribution:",
    ]

    for q, count in schedule.audits_by_quarter.items():
        bar = "█" * count + "░" * (10 - count)
        lines.append(f"  {q}: {bar} {count}")

    lines.extend([
        "",
        "-" * 70,
        "AUDIT SCHEDULE",
        "-" * 70,
        f"{'Process':<25} {'Clause':<8} {'Date':<12} {'Risk':<8} {'Priority':<8}",
        "-" * 70,
    ])

    for audit in schedule.schedule:
        lines.append(
            f"{audit['process_name']:<25} "
            f"{audit['iso_clause']:<8} "
            f"{audit['scheduled_date']:<12} "
            f"{audit['risk_level']:<8} "
            f"{audit['priority_score']:<8}"
        )

    lines.extend([
        "",
        "-" * 70,
        "RECOMMENDATIONS",
        "-" * 70,
    ])

    for i, rec in enumerate(schedule.recommendations, 1):
        lines.append(f"{i}. {rec}")

    lines.append("=" * 70)
    return "\n".join(lines)


def interactive_mode():
    """Run interactive schedule generation."""
    print("=" * 60)
    print("Audit Schedule Optimizer - Interactive Mode")
    print("=" * 60)

    processes = []
    print("\nEnter processes (blank name to finish):\n")

    while True:
        name = input("Process name (or Enter to finish): ").strip()
        if not name:
            break

        clause = input("ISO 13485 clause (e.g., 7.3): ").strip()
        risk = input("Risk level (H/M/L): ").strip().upper()
        risk_level = {
            "H": RiskLevel.HIGH,
            "M": RiskLevel.MEDIUM,
            "L": RiskLevel.LOW
        }.get(risk, RiskLevel.MEDIUM)

        last_audit = input("Last audit date (YYYY-MM-DD, or Enter if never): ").strip()
        if not last_audit:
            last_audit = None

        findings = input("Previous findings count (default 0): ").strip()
        findings = int(findings) if findings.isdigit() else 0

        processes.append(Process(
            name=name,
            iso_clause=clause,
            risk_level=risk_level,
            last_audit_date=last_audit,
            previous_findings=findings
        ))

        print(f"Added: {name}\n")

    if not processes:
        print("No processes entered. Using default ISO 13485 processes.")
        processes = [
            Process(name=name, iso_clause=clause, risk_level=RiskLevel.MEDIUM)
            for name, clause in AuditScheduleOptimizer.REQUIRED_PROCESSES
        ]

    optimizer = AuditScheduleOptimizer(processes)
    schedule = optimizer.generate_schedule()
    print("\n" + format_text_output(schedule))


def main():
    parser = argparse.ArgumentParser(
        description="Risk-Based Audit Schedule Optimizer"
    )
    parser.add_argument(
        "--processes",
        type=str,
        help="JSON file with process definitions"
    )
    parser.add_argument(
        "--output",
        choices=["text", "json"],
        default="text",
        help="Output format"
    )
    parser.add_argument(
        "--interactive",
        action="store_true",
        help="Run in interactive mode"
    )
    parser.add_argument(
        "--months",
        type=int,
        default=12,
        help="Planning horizon in months"
    )

    args = parser.parse_args()

    if args.interactive:
        interactive_mode()
        return

    if args.processes:
        with open(args.processes, "r") as f:
            data = json.load(f)

        processes = []
        for p in data.get("processes", []):
            risk = RiskLevel[p.get("risk_level", "MEDIUM").upper()]
            processes.append(Process(
                name=p["name"],
                iso_clause=p.get("iso_clause", ""),
                risk_level=risk,
                last_audit_date=p.get("last_audit_date"),
                previous_findings=p.get("previous_findings", 0),
                criticality_score=p.get("criticality_score", 5)
            ))
    else:
        # Use default processes
        processes = [
            Process(name=name, iso_clause=clause, risk_level=RiskLevel.MEDIUM)
            for name, clause in AuditScheduleOptimizer.REQUIRED_PROCESSES
        ]

    optimizer = AuditScheduleOptimizer(processes)
    schedule = optimizer.generate_schedule(args.months)

    if args.output == "json":
        print(json.dumps(asdict(schedule), indent=2))
    else:
        print(format_text_output(schedule))


if __name__ == "__main__":
    main()
