"""CLI interface for fosho commands using Typer."""

import sys
from pathlib import Path
from typing import Optional

import typer
from rich.console import Console
from rich.table import Table

from .hashing import compute_file_crc32, compute_schema_md5
from .manifest import Manifest
from .scaffold import scaffold_dataset_schema

app = typer.Typer(
    name="fosho",
    help="Wrapper for pandera schema checking. Usage (with uv): \n\n"
    "1. uv run fosho scan path/to/data\n\n"
    "# (then check the schemas generated in the schemas folder, probably modify or ask an LLM to write a better one, its autogenerated)\n\n"
    "1.5. uv run fosho status\n\n"
    "# (check the status)\n\n"
    "2. uv run fosho sign\n\n"
    "# (and check the status within any step via)\n\n"
    "3. uv run fosho status\n\n"
    "# (check the status)",
)
console = Console()


@app.command()
def scan(
    path: str = typer.Argument(..., help="Directory to scan for datasets"),
    overwrite_schemas: bool = typer.Option(
        False, "--overwrite-schemas", help="Regenerate schema stubs even if they exist"
    ),
):
    """Scan directory for datasets, generate schemas, and update manifest."""
    scan_path = Path(path)

    if not scan_path.exists():
        console.print(f"[red]Error: Path {path} does not exist[/red]")
        sys.exit(1)

    if not scan_path.is_dir():
        console.print(f"[red]Error: Path {path} is not a directory[/red]")
        sys.exit(1)

    # Find all CSV and Parquet files, excluding common directories to avoid
    excluded_dirs = {
        ".venv",
        "venv",
        "__pycache__",
        ".git",
        "node_modules",
        ".pytest_cache",
    }
    data_files = []
    for pattern in ["**/*.csv", "**/*.parquet", "**/*.pq"]:
        for file_path in scan_path.glob(pattern):
            # Check if any parent directory is in excluded_dirs
            if any(part in excluded_dirs for part in file_path.parts):
                continue
            data_files.append(file_path)

    if not data_files:
        console.print(f"[yellow]No CSV or Parquet files found in {path}[/yellow]")
        return

    console.print(f"Found {len(data_files)} dataset(s)")

    # Load or create manifest
    manifest = Manifest()
    manifest.load()

    # Process each file
    for file_path in data_files:
        try:
            relative_path = str(file_path.relative_to(Path.cwd()))
        except ValueError:
            # If file is not relative to cwd, use absolute path
            relative_path = str(file_path)
        console.print(f"Processing: {relative_path}")

        try:
            # Compute file hash
            crc32_hash = compute_file_crc32(file_path)

            # Scaffold schema
            schema, schema_file = scaffold_dataset_schema(
                file_path, overwrite=overwrite_schemas
            )

            # Compute schema hash
            schema_md5 = compute_schema_md5(schema)

            # Check if file changed
            existing_entry = manifest.get_dataset(relative_path)
            if existing_entry:
                if (
                    existing_entry["crc32"] != crc32_hash
                    or existing_entry["schema_md5"] != schema_md5
                ):
                    # File or schema changed, mark as unsigned
                    manifest.add_dataset(
                        relative_path, crc32_hash, schema_md5, signed=False
                    )
                    console.print(
                        f"  [yellow]Updated (unsigned due to changes)[/yellow]"
                    )
                else:
                    console.print(f"  [green]No changes[/green]")
            else:
                # New file
                manifest.add_dataset(
                    relative_path, crc32_hash, schema_md5, signed=False
                )
                console.print(f"  [blue]Added (unsigned)[/blue]")

            if schema_file:
                console.print(f"  Schema: {schema_file}")

        except Exception as e:
            console.print(f"  [red]Error: {e}[/red]")

    # Save manifest
    manifest.save()
    console.print(f"[green]Manifest updated: manifest.json[/green]")


@app.command()
def sign():
    """Sign all datasets in manifest (mark as approved)."""
    manifest = Manifest()
    manifest.load()

    datasets = manifest.get_all_datasets()
    if not datasets:
        console.print("[yellow]No datasets found in manifest[/yellow]")
        sys.exit(1)

    # Verify all files exist, checksums match, and schemas validate
    console.print("Verifying datasets before signing...")

    for file_path, dataset_info in datasets.items():
        file_obj = Path(file_path)

        if not file_obj.exists():
            console.print(f"[red]Error: File {file_path} not found[/red]")
            sys.exit(1)

        # Verify checksum
        current_crc32 = compute_file_crc32(file_obj)
        if current_crc32 != dataset_info["crc32"]:
            console.print(f"[red]Error: Data has changed in {file_path}[/red]")
            console.print(f"  Previous checksum: {dataset_info['crc32']}")
            console.print(f"  Current checksum:  {current_crc32}")
            console.print("\n[yellow]To resolve this:[/yellow]")
            console.print("  1. If the changes are expected:")
            console.print(
                "     Run 'fosho scan' to update the manifest with the new data"
            )
            console.print("     Then run 'fosho sign' to approve the changes")
            console.print("  2. If the changes are unexpected:")
            console.print("     Check the file contents to understand what changed")
            console.print("     Restore the file to its previous state if needed")
            sys.exit(1)

        # Verify schema validates against actual data
        try:
            from .scaffold import scaffold_dataset_schema
            from .reader import read_csv_with_schema

            schema, _ = scaffold_dataset_schema(file_obj, overwrite=False)
            current_schema_md5 = compute_schema_md5(schema)

            # Check schema hash matches
            if current_schema_md5 != dataset_info["schema_md5"]:
                console.print(f"[red]Error: Schema mismatch for {file_path}[/red]")
                console.print(f"  Expected schema MD5: {dataset_info['schema_md5']}")
                console.print(f"  Current schema MD5:  {current_schema_md5}")
                console.print(f"  Run 'fosho scan' to update the manifest")
                sys.exit(1)

            # Validate data against schema
            df = read_csv_with_schema(file_obj, schema)
            validated_df = df.validate()  # This will raise if schema doesn't fit data
            console.print(f"  [green]✓ Schema validates {file_path}[/green]")

        except Exception as e:
            console.print(f"[red]Error: Schema validation failed for {file_path}[/red]")
            console.print(f"  {str(e)}")
            console.print(f"  Please fix the schema (or data?!) before signing")
            sys.exit(1)

    # All verifications passed, sign all datasets
    manifest.sign_all()
    manifest.save()

    console.print(f"[green]Successfully signed {len(datasets)} dataset(s)[/green]")


@app.command()
def status(
    json_output: bool = typer.Option(False, "--json", help="Output status as JSON for machine parsing")
):
    """Show status of all datasets with file, data, and schema verification.\n\n

    # Column summaries: \n\n
    # - Dataset: The path to the data file \n\n
    # - CRC32: The CRC32 hash, generated via `crc32`. Not configurable yet \n\n
    # - Status: Signed or Unsigned, via the `sign` command within fosho. Checks the manifest.json file \n\n
    # - File Status: Exists or missing, referencing the file path and what the manifest.json expects \n\n
    # - Data Status: When the data changes (it happens), we want to flag this as "invalid" so the diligent user re-checks stuff and re-signs \n\n
    # - Schema Status: Similar to data status but with the schema. \n\n
    # - Schema Path: The path to the corresponding schema file \n\n
    # - Signed At: The date and time the dataset was signed
    """
    manifest = Manifest()
    manifest.load()

    datasets = manifest.get_all_datasets()
    if not datasets:
        console.print("[yellow]No datasets found in manifest[/yellow]")
        return

    # Verify manifest integrity first
    manifest_integrity = manifest.verify_integrity()
    if not manifest_integrity and not json_output:
        console.print("[red]Warning: Manifest integrity check failed[/red]")

    if not json_output:
        # Hint user to run with --help to understand the columns
        console.print("[yellow]Hint: Run with --help for column descriptions[/yellow]")

        table = Table()
        table.add_column("Dataset", style="cyan")
        table.add_column("CRC32", style="magenta")
        table.add_column("Status", style="green")
        table.add_column("File Status")
        table.add_column("Data Status")
        table.add_column("Schema Status")
        table.add_column("Schema Path", style="blue")
        table.add_column("Signed At")

    changes_detected = False

    for file_path, info in datasets.items():
        file_obj = Path(file_path)

        # Check file existence
        if file_obj.exists():
            file_status = "[green]✓ Exists[/green]"

            # Check data integrity (CRC32)
            try:
                current_crc32 = compute_file_crc32(file_obj)
                data_match = current_crc32 == info["crc32"]
                data_status = (
                    "[green]✓ Unchanged[/green]"
                    if data_match
                    else "[red]✗ Modified[/red]"
                )

                # Check schema if file exists and we can load it
                try:
                    from .scaffold import scaffold_dataset_schema

                    schema, schema_file = scaffold_dataset_schema(
                        file_obj, overwrite=False
                    )
                    current_schema_md5 = compute_schema_md5(schema)
                    schema_match = current_schema_md5 == info["schema_md5"]
                    schema_status = (
                        "[green]✓ Matches Data[/green]"
                        if schema_match
                        else "[yellow]⚠ Out of Sync[/yellow]"
                    )
                    schema_path = (
                        str(schema_file) if schema_file else "[gray]None[/gray]"
                    )
                except Exception:
                    schema_status = "[gray]? Unknown[/gray]"
                    schema_match = True  # Don't treat as error if we can't check
                    schema_path = "[gray]Unknown[/gray]"

                # Mark as unsigned if data changed
                if not data_match and info["signed"]:
                    manifest.unsign_dataset(file_path)
                    changes_detected = True
                    info["signed"] = False
                    info["signed_at"] = None

            except Exception as e:
                data_status = f"[red]✗ Error: {str(e)[:20]}...[/red]"
                schema_status = "[gray]? Unknown[/gray]"
                schema_path = "[gray]Unknown[/gray]"
        else:
            file_status = "[red]✗ Not Found[/red]"
            data_status = "[red]✗ File Missing[/red]"
            schema_status = "[red]✗ Can't Check[/red]"
            schema_path = "[red]Not Found[/red]"

        # Overall status - only add to table if not JSON output
        if not json_output:
            status_text = "✓ Signed" if info["signed"] else "✗ Unsigned"
            status_style = "green" if info["signed"] else "yellow"

            table.add_row(
                file_path,
                info["crc32"],
                f"[{status_style}]{status_text}[/{status_style}]",
                file_status,
                data_status,
                schema_status,
                schema_path,
                info["signed_at"] or "Never",
            )

    if not json_output:
        console.print(table)

    # Save manifest if we unmarked any datasets due to data changes
    if changes_detected:
        manifest.save()
        if not json_output:
            console.print(
                "\n[yellow]Note: Some datasets were automatically unsigned due to data changes[/yellow]"
            )

    # Summary counts and verification status
    total = len(datasets)
    signed_count = sum(1 for info in datasets.values() if info["signed"])

    # Collect all issues that would prevent successful downstream usage
    issues = []

    # Check for unsigned datasets
    unsigned_count = total - signed_count
    if unsigned_count > 0:
        issues.append(f"{unsigned_count} dataset(s) not signed")

    # Check for missing files
    missing_files = [
        path
        for path, info in datasets.items()
        if not Path(path).exists() and info["signed"]
    ]
    if missing_files:
        issues.append(f"{len(missing_files)} signed dataset(s) missing")

    # Check for data changes after signing
    changed_data = []
    changed_schemas = []
    for path, info in datasets.items():
        if info["signed"] and Path(path).exists():
            try:
                current_crc32 = compute_file_crc32(Path(path))
                if current_crc32 != info["crc32"]:
                    changed_data.append(path)

                schema, _ = scaffold_dataset_schema(Path(path), overwrite=False)
                current_schema_md5 = compute_schema_md5(schema)
                if current_schema_md5 != info["schema_md5"]:
                    changed_schemas.append(path)
            except Exception:
                # If we can't verify, treat as an issue
                issues.append(f"Cannot verify '{path}' integrity")

    if changed_data:
        issues.append(f"{len(changed_data)} signed dataset(s) modified after signing")
    if changed_schemas:
        issues.append(f"{len(changed_schemas)} schema(s) changed after signing")

    # Prepare status data for JSON output or exit code determination
    status_data = {
        "total_datasets": total,
        "signed_datasets": signed_count,
        "verification_passed": len(issues) == 0,
        "issues": issues,
        "datasets": {}
    }
    
    # Add detailed dataset info for JSON
    for path, info in datasets.items():
        file_obj = Path(path)
        dataset_status = {
            "signed": info["signed"],
            "signed_at": info["signed_at"],
            "crc32": info["crc32"],
            "schema_md5": info["schema_md5"],
            "file_exists": file_obj.exists(),
            "data_unchanged": True,
            "schema_unchanged": True
        }
        
        if file_obj.exists():
            try:
                current_crc32 = compute_file_crc32(file_obj)
                dataset_status["data_unchanged"] = current_crc32 == info["crc32"]
                
                schema, _ = scaffold_dataset_schema(file_obj, overwrite=False)
                current_schema_md5 = compute_schema_md5(schema)
                dataset_status["schema_unchanged"] = current_schema_md5 == info["schema_md5"]
            except Exception:
                dataset_status["data_unchanged"] = False
                dataset_status["schema_unchanged"] = False
        
        status_data["datasets"][path] = dataset_status

    if json_output:
        import json
        console.print(json.dumps(status_data, indent=2))
    else:
        # Print human-readable output
        console.print(f"\nSummary: {signed_count}/{total} datasets signed")

        if issues:
            console.print("\n[red]❌ fosho verification failed:[/red]")
            for issue in issues:
                console.print(f"[red]  - {issue}[/red]")
            console.print(
                "\n[yellow]Hint: Run 'fosho scan' to update manifest, then check and re-sign affected datasets[/yellow]"
            )
        else:
            console.print(
                "\n[green]✓ fosho verification passed - all datasets are signed and verified[/green]"
            )
    
    # Exit with appropriate code for machine parsing
    if issues:
        sys.exit(1)  # Verification failed
    else:
        sys.exit(0)  # Verification passed


if __name__ == "__main__":
    app()
